QR 스캐너 게이트 수정: DeviceType 휴리스틱 오판(실기기에서 스캐너 미표시) — iOS 시뮬레이터만 폴백, Android 는 항상 스캐너
This commit is contained in:
parent
0cb16c8830
commit
bbbd83fe58
6 changed files with 5 additions and 1 deletions
219
docs/BACKLOG.md
Normal file
219
docs/BACKLOG.md
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
# Video Downloader — 개선 백로그
|
||||
|
||||
> **출처**: 2026-07-26 다차원 코드 감사(9개 차원, 원시 63개 → 상위 15 선정). 모두 실제 소스 기반(file:line 검증).
|
||||
> **사용법**: 완료한 항목은 `- [ ]` → `- [x]` 로 체크.
|
||||
|
||||
## 🏆 우선 구현 추천 (TOP 3)
|
||||
1. **#1 다운로드 취소 버튼** — 멈출 수 없는 치명 UX 결함, 코드에 Cts만 있어 한 줄이면 끝.
|
||||
2. **#2 ffmpeg stderr 드레인 + Kill** — hang/데드락/좀비 프로세스 안전 결함 + 리먹스 실패 노출.
|
||||
3. **#5 동시 다운로드 전역 제한(SemaphoreSlim)** — 소켓 고갈(#11)과 직결, 정적 필드 하나짜리 퀵윈.
|
||||
|
||||
---
|
||||
|
||||
## S 공수 (빠른 승리 — #1~#10)
|
||||
|
||||
### [x] 1. 다운로드 취소 버튼이 UI에 없음
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.App/MainWindow.xaml`, `Core/DownloadManager.cs`
|
||||
- **문제**: 카드에 받기/재시도/폴더만 있고 취소 없음. `item.Cts` 토큰은 만들지만 `.Cancel()` 호출 0건 → 다운로드를 멈출 방법 자체가 없음.
|
||||
- **해결**: 카드에 '취소' 버튼 → `item.Cts?.Cancel()`. 진행 중(다운로드/병합/리먹스)일 때만 활성화.
|
||||
|
||||
### [x] 2. ffmpeg stderr 미읽기 → 데드락 + 좀비 프로세스 + 리먹스 실패 조용 폭백
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.Core/Hls/HlsDownloader.cs` (`RunFfmpegAsync`, `RemuxAsync`)
|
||||
- **문제**: stderr 리다이렉트만 켜두고 안 읽음 → 파이프 버퍼 차면 ffmpeg hang. 취소 시 Kill 없이 좀비. 실패해도 `.ts`로 조용히 넘어감.
|
||||
- **해결**: `WaitForExitAsync`와 `StandardError.ReadToEndAsync` 병렬 대기(로그 저장), finally에서 `Kill(entireProcessTree)`, exit≠0이면 예외 throw → `item.Error` 표시. partial mp4 정리.
|
||||
|
||||
### [x] 3. explorer.exe 인자 인젝션
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.App/MainWindow.xaml.cs` (CardFolder/OpenFolder)
|
||||
- **문제**: `Process.Start("explorer.exe", $"...{path}...")` 문자열 인자 직접 전달. OutputFolder(config 평문) 변조 시 `/root,` 스위치로 인자 조작 가능.
|
||||
- **해결**: ProcessStartInfo 명시 + `Path.GetFullPath` 정규화 + OutputFolder 하위 검증(또는 `UseShellExecute=true`).
|
||||
|
||||
### [x] 4. NewWindowRequested 미처리 → 팝업이 앱 통제 이탈
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.App/MainWindow.xaml.cs` (`InitializeBrowserAsync`)
|
||||
- **문제**: `target=_blank`/`window.open()` 팝업이 별도 창 → 감지/쿠키 안 타서 팝업 안 영상은 대기열에 안 잡힘.
|
||||
- **해결**: `cv.NewWindowRequested`에서 현재 WebView2로 강제 라우팅(`e.NewWindow = (CoreWebView2)s; e.Handled = true`).
|
||||
|
||||
### [x] 5. 동시 다운로드 수 전역 제한 없음
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.App/Core/DownloadManager.cs` (`StartDownload`)
|
||||
- **문제**: StartDownload가 fire-and-forget. N개 누르면 N × Concurrency개 TCP 동시 요청 폭증 → 소켓 고갈(#11) 가속.
|
||||
- **해결**: `static SemaphoreSlim(MaxConcurrentDownloads)` 도입, StartDownload에서 WaitAsync/Release. 세그먼트 단위 Concurrency와 분리해 AppConfig에 각각 설정.
|
||||
|
||||
### [x] 6. .parts 디렉토리 정리 누락 → 디스크 누수 + 이름 cascade
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.Core/Hls/HlsDownloader.cs`, `Core/DownloadManager.cs` (`MakeUnique`)
|
||||
- **문제**: 완료/실패/취소 어느 경로든 partsDir 안 지움 → 수천 세그먼트 영구 축적. 잔류 `.parts`가 `MakeUnique` 밀어내 `(1)(2)...` 접미 연쇄.
|
||||
- **해결**: 성공 시 `Directory.Delete(partsDir, recursive)`. 실패/취소 시에도 정리(이어받기 토글 옵션).
|
||||
|
||||
### [x] 7. 전역 키보드 단축키 전무
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.App/MainWindow.xaml`
|
||||
- **문제**: Ctrl+L(주소), F5/Ctrl+R(새로고침), Esc(정지), Alt+←/→(뒤로/앞으로), Ctrl+F(검색), Ctrl+±/0(줌) 어느 것도 안 됨.
|
||||
- **해결**: `Window.InputBindings`에 KeyBinding 추가.
|
||||
|
||||
### [x] 8. requests.log 무한 증가 (로테이션 전무)
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.App/Core/DownloadManager.cs` (`AppendAll`, `OnResponseReceived`)
|
||||
- **문제**: 모든 응답(이미지/광고/분석 포함) 기록, 크기 제한 없음 → 세션당 GB 가능, OutputFolder 디스크 채움.
|
||||
- **해결**: 10MB 임계 롤오버 + truncate. 관심 없는 content-type(status≥300/image/font/css/js/beacon) 스킵. 설정 토글로 끄기.
|
||||
|
||||
### [x] 9. 직링크 Content-Length 미상 시 진행률 0% 고정 (멈춤 오인)
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.Core/Hls/DirectDownloader.cs`, `Models/DownloadProgress.cs`, `MainWindow.xaml`
|
||||
- **문제**: chunked 응답에서 pct=0 → ProgressBar 내내 0%. Segments엔 '1.2 MB'만 떠 멈춘 것처럼 보임.
|
||||
- **해결**: 길이 미상(OverridePercent=-1) 시 `IsIndeterminate=true`, Detail(바이트)은 계속 표시.
|
||||
|
||||
### [x] 10. 다운로드 실패 원인(Error)이 UI에 안 보임
|
||||
- **임팩트**: 높 · **공수**: S · **파일**: `VideoDownloader.App/MainWindow.xaml` (카드 템플릿), `ViewModels/DownloadItemViewModel.cs`
|
||||
- **문제**: `item.Error`는 채워지는데 카드엔 Status만 바인딩 → '오류' 단어만. '404'/'디스크 가득' 등 원인은 app.log 열어야 앎. `ErrorBrush`도 미사용.
|
||||
- **해결**: 카드에 `{Binding Error}` TextBlock(ErrorBrush, Status=='오류'일 때만 노출) + 실패 카드 보더/전경색 DataTrigger.
|
||||
|
||||
---
|
||||
|
||||
## M 공수 (#11~#15)
|
||||
|
||||
### [x] 11. 호출마다 new HttpClient + Dispose 안 함 (소켓 고갈)
|
||||
- **임팩트**: 높 · **공수**: M · **파일**: `VideoDownloader.App/Core/DownloadManager.cs` (`BuildHttpClientAsync`)
|
||||
- **문제**: 매 호출마다 새 HttpClient(handler+client). 항목당 2개. TIME_WAIT ~240초 점유 → 포트 고갈.
|
||||
- **해결**: 앱 수명 단일 공유 HttpClient(SocketsHttpHandler + PooledConnectionLifetime). 요청별 헤더(쿠키/Referer/UA)는 HttpRequestMessage로.
|
||||
|
||||
### [x] 12. 직링크에 재시도·이어받기·임시파일 전무
|
||||
- **임팩트**: 높 · **공수**: M · **파일**: `VideoDownloader.Core/Hls/DirectDownloader.cs`
|
||||
- **문제**: GetAsync 1회로 끝. 드롭 시 0바이트부터 재시작. `File.Create(최종이름)` 직접 쓰기 → 중단 시 손상 파일이 최종명으로 남음.
|
||||
- **해결**: `.part` 임시파일 + `Range: bytes=written-` 이어받기 + 재시도 루프(4xx 영구 오류 제외, Retry-After) + 완료 시 `File.Move`.
|
||||
|
||||
### [x] 13. WebView2 기본 다운로드가 대기열/OutputFolder 우회
|
||||
- **임팩트**: 높 · **공수**: M · **파일**: `VideoDownloader.App/MainWindow.xaml.cs` (`InitializeBrowserAsync`)
|
||||
- **문제**: 페이지 내 `a[download]` 링크 클릭 시 WebView2 자체 다운로더 → 사용자 기본 폴더. `DownloadStarting` 구독 없음 → 앱 대기열/카드에 안 잡힘.
|
||||
- **해결**: `cv.DownloadStarting`에서 ResultFilePath를 OutputFolder로 강제 또는 Cancel 후 대기열로 라우팅.
|
||||
|
||||
### [x] 14. 글로벌 예외 핸들러/크래시 리포트 부재
|
||||
- **임팩트**: 높 · **공수**: M · **파일**: `VideoDownloader.App/App.xaml.cs`
|
||||
- **문제**: `App.xaml.cs` 빈 클래스. `DispatcherUnhandledException`/`TaskScheduler.UnobservedTaskException` 없음 → fire-and-forget 예외 조용 삼켜짐, 크래시 시 '작동 중지'만.
|
||||
- **해결**: 세 핸들러 연결 → app.log에 스택 기록 + 요약 메시지. fire-and-forget에 `ContinueWith(OnlyOnFaulted)` 관찰.
|
||||
|
||||
### [x] 15. 감지 신뢰성: image/* 위장·청크·후보 미등록이 미디어 누락으로
|
||||
- **임팩트**: 높 · **공수**: M · **파일**: `VideoDownloader.App/Detection/MediaDetector.cs`, `MainWindow.xaml.cs` (`OnResponseReceived`)
|
||||
- **문제**: (a) `MatchFormat`에 image/* 분기 없음 → tvchak식 `play.png`+`image/png` 위장 mp4/TS 100% 누락. (b) chunked 응답은 크기 가드 통과 → 토큰 만료 HTML 에러 페이지도 mp4로 오탐. (c) `.mpd`/`.m4s`/octet-stream 후보는 로그만 찍고 항목 등록 안 됨.
|
||||
- **해결**: 다운로드 첫 GET 시 magic byte 검증(MP4 ftyp / TS 0x47 / WebM 0x1A45DFA3 / MP3), MatchFormat에 image/*+대형 분기, 후보도 OnDetected 전달, chunked는 사후 길이 재검증.
|
||||
|
||||
---
|
||||
|
||||
## 메모
|
||||
- S 공수 10개(#1~#10)는 반나절 안에 일괄 처리 가능.
|
||||
- #5와 #11은 함께 잡으면 시너지(소켓 고갈 근본 해결).
|
||||
- #15는 감지 신뢰성의 핵심 — 이 사이트(tvchak 계열 image/png 위장)에 직결.
|
||||
|
||||
---
|
||||
|
||||
## 팔로업 (2026-07-26 완료)
|
||||
- [x] **라이트 테마 토글** — `Themes/Shared.xaml`(스타일) + `Dark/Light.xaml`(색). 설정 체크박스 + 변경 시 재시작. DWM 타이틀바 색 연동.
|
||||
- [x] **MaxConcurrentDownloads UI 노출** — 설정에 "최대 동시 항목" 필드. (다음 시작부터 적용)
|
||||
- [x] **DASH(.mpd) 지원** — `Core/Dash/` (DashParser/DashDownloader). 클리어 콘텐츠만. 샘플 MPD 파싱 검증 통과.
|
||||
- [x] **보더리스 윈도우** — WindowChrome + Fluent 캡션버튼 + 툴바 전체 드래그 + Win11 둥근 모서리.
|
||||
|
||||
## 2차 팔로업 (2026-07-27 완료)
|
||||
- [x] **DASH A/V 통합 + ffmpeg-native** — 수동 세그먼트 병합(fMP4)이 깨지기 쉬워 `ffmpeg -i <mpd>` 로 위임. DashParser는 DRM 사전검증/진행률(duration)용. BBB 공개스트림 종단간 검증(h264+aac mp4 생성).
|
||||
- [x] **감지 오탐 감소** — GIF 트래킹 픽셀이 TS(0x47='G')로 오탐되던 것 수정. ProbeFormat이 이미지 매직바이트(GIF/PNG/JPEG/WebP) 제외.
|
||||
- [x] **MaxConcurrentDownloads 런타임 반영** — 동적 게이트(증가=즉시 Release, 감소=완료 시 미충전으로 자연 수축). 설정 저장 시 즉시 적용.
|
||||
- [x] **라이트 테마 시각 점검** — 팔레트 대비 건전(어두운 텍스트 #1B1C24 / 밝은 배경).
|
||||
- [x] **주소창 자동완성/제안** — 히스토리 접두사/부분일치 드롭다운 + 인라인 자동완성(Tab/→ 확정, ↑↓ 탐색, Enter 이동, 클릭 이동).
|
||||
- [x] **인스타/blob·Range 영상 감지** — 206 응답을 Content-Range 전체크기로 판정(청크가 작아 skip되던 것). IG 실계정 테스트는 불가(로그인/지역).
|
||||
- [x] **IndeterminateBar 스토리보드 예외 수정** — TranslateTransform 의 x:Name(템플릿 namescope 미등록) → Border 이름 + RenderTransform 속성경로로 타겟.
|
||||
|
||||
## 3차 팔로업 (2026-07-29 완료) — 쿠키/YouTube + 감사 결함 일괄 수정
|
||||
> 9개 차원 다차원 감사(24 에이전트 워크플로우, 원시 42개 → 확정 11개 결함) 기반. 사용자 제보(YouTube 다운로드 시 yt-dlp 가 쿠키 파일 거부)에서 시작.
|
||||
|
||||
### 치명 — YouTube/쿠키 (사용자 제보 직결)
|
||||
- [x] **쿠키 파일 UTF-8 BOM → yt-dlp 거부** — `CookieJarWriter` 가 `Encoding.UTF8`(BOM 포함) 로 써 첫 줄 헤더 `# Netscape HTTP Cookie File` 인식이 깨짐 → "does not look like a Netscape format cookies file" + "invalid length 1". `new UTF8Encoding(false)` 로 BOM 제거. HttpOnly 쿠키에 `#HttpOnly_` 접두 추가, 값 내 tab/CRLF sanitize. (`Core/Social/CookieJarWriter.cs`)
|
||||
- [x] **YouTube player_client 오버라이드가 yt-dlp 기본값보다 못했음** — 하드코딩 `android_vr,android,web_sns,web` 이 2026년엔 대부분 무효: `android`/`ios` 는 GVS/Player **PO 토큰 필수 + 쿠키 미지원**, `web` 도 PO 토큰 없으면 SABR 만, `web_sns` 는 yt-dlp 에 **존재하지 않는 클라이언트**. → 오버라이드를 비우면 yt-dlp 공식 기본값(`visionos,android_vr,web`, 유지보수자 갱신) 사용. 고급 설정 필드 `YoutubePlayerClients`(설정 UI)로 보존. (`Core/Social/YtDlpDownloader.cs`, `Core/Config/AppConfig.cs`, `SettingsWindow`)
|
||||
|
||||
### 백로그 "완료" 표기 중 실제 미구현이었던 것
|
||||
- [x] **#12 Retry-After 미적용** — 백로그엔 "Retry-After 적용" 이라 적었으나 소스엔 한 줄도 없었음. `DirectDownloader` 가 429/503 시 서버 `Retry-After`(최대 60s 캡) 를 존중하도록 수정. (`Core/Hls/DirectDownloader.cs`)
|
||||
- [x] **#15(b) chunked video/* 사후 길이 재검증 누락** — Content-Length 없는 video/* 응답이 크기 가드를 통과해 토큰 만료 HTML 오류 페이지가 mp4 로 오탐. chunked video/* 를 magic-byte probe 로 재검증(정상 MP4 는 ftyp box 로 통과). (`MainWindow.xaml.cs`)
|
||||
|
||||
### 코드 헬스 (감사 확정)
|
||||
- [x] **공유 CookieContainer 프로세스 수준 누적** — `SharedHandler` 의 컨테이너에 매 다운로드마다 쿠키가 누적/타사이트 누출. `UseCookies=false` + 요청(다운로드) 단위 `Cookie` 헤더로 스코프. (`Core/DownloadManager.cs`)
|
||||
- [x] **DASH ffmpeg 헤더 누락** — `ffmpeg -i <mpd>` 가 UA/Referer/쿠키 없이 기본 UA 로 받아 토큰·Referer 인증 CDN(maccms 계열) 에서 403. `-user_agent`/`-referer`/`-headers` 를 `-i` 앞에 전달. (`Core/Dash/DashDownloader.cs`)
|
||||
- [x] **DashParser $Time$ → 세그먼트 번호 오탐** — `$Time$` 를 번호로 치환하던 것(ISO/IEC 23009-1 위반) 을 미디어 시작시각(t/d 누적) 으로 수정. (`Core/Dash/DashParser.cs`)
|
||||
- [x] **DashParser r=-1 → 세그먼트 0개** — SegmentTimeline `r="-1"`(Period 끝까지 반복) 을 `count=r+1=0` 으로 처리해 표현이 통째로 사라지던 것 수정. (`Core/Dash/DashParser.cs`)
|
||||
- [x] **HlsDownloader stdout 드레인 태스크 미관측** — 취소 시 `_ =` 폐기 태스크가 미관측 상태로 남던 것을 finally 에서 await. (`Core/Hls/HlsDownloader.cs`)
|
||||
- [x] **BrowserStore 조용 예외 삼킴** — 즐겨찾기/히스토리 저장·로드 실패를 완전 무시(조용 데이터 손실) 하던 것을 `browserstore.log` 기록 + 손상 파일 `.corrupt.bak` 백업. (`Browser/BrowserStore.cs`)
|
||||
|
||||
### 검증
|
||||
- `dotnet build` 0 오류 / 0 경고.
|
||||
- 셀프테스트(임시 `--selftest`): 쿠키파일 BOM 없음·HttpOnly 접두·Netscape 헤더 + DashParser r=-1(5 세그먼트)/`$Time$(0,8000)` 8/8 PASS.
|
||||
- 실제 공개 DASH(BBB) 매니페스트 파싱 정상(VIDEO/AUDIO 각 159 세그먼트) — `$Number$` 경로 회귀 없음.
|
||||
|
||||
## 4차 팔로업 (2026-07-29 완료) — 브라우저 프로필 + Private 모드 + 범용 패널
|
||||
### 브라우저 데이터 보존/관리 (사용자 요청)
|
||||
- [x] **WebView2 영속 프로필** — 기본(exe 디렉터리 내) user data 폴더는 빌드/재배포 시 날아가 로그인이 풀렸음. `%AppData%/VideoDownloader/webview` 로 명시(`CoreWebView2Environment.CreateAsync`) → 쿠키·세션·localStorage·캐시가 앱/빌드 재시작에도 보존. (`MainWindow.xaml.cs`)
|
||||
- [x] **Private(InPrivate) 모드** — `CoreWebView2ControllerOptions.IsInPrivateModeEnabled` 로 Edge InPrivate 세션(디스크에 아무것도 저장 안 함). 툴바 토글 버튼(활성 시 강조) → 확인 → `AppConfig.PrivateMode` 저장 → 재시작 적용(테마와 동일 패턴).
|
||||
- [x] **브라우저 데이터 관리** — 설정창에 프로필 경로 표시 + "삭제" 버튼(`CoreWebView2Profile.ClearBrowsingDataAsync` → 쿠키·DOM 저장소·캐시 정리). (`SettingsWindow`)
|
||||
### UI (사용자 요청)
|
||||
- [x] **범용 우측 패널** — 다운로드 컬럼을 접기/펼치기(헤더 토글, 접힘 시 좁은 스트립) + GridSplitter 폭 리사이즈(280~900) + `RightPanelWidth`/`RightPanelCollapsed` 저장·복원. (`MainWindow.xaml/.cs`)
|
||||
- [x] **진행률 바 미작동 수정** — `AccentProgressBar` 템플릿에 `PART_Track` 이름이 없어 WPF 가 인디케이터 폭을 계산 못 함(바가 안 채워짐). `x:Name="PART_Track"` 추가. (`Themes/Shared.xaml`)
|
||||
### 검증
|
||||
- `dotnet build` 0 오류 / 0 경고. 앱 시작 스모크 테스트(새 WebView2 초기화 경로) 7s 생존 확인 — 크래시 없음.
|
||||
|
||||
## 5차 팔로업 (2026-07-29 완료) — YouTube n 챌린지(EJS) 다운로드 실패
|
||||
> 사용자 제보: YouTube 영상 다운로드 시 yt-dlp 가 `n challenge solving failed` + `Only images are available for download` 로 MP4 실패(exit 1).
|
||||
|
||||
### 치명 — YouTube (사용자 제보 직결)
|
||||
- [x] **YouTube n 챌린지 해석 미설정 → 포맷 드랍/스로틀** — YouTube 가 플레이어 응답의 "n" 서명 스로틀 파라미터를 JS 로 난독화. yt-dlp 는 이를 풀기 위해 JS 런타임(Deno) 으로 실행되는 EJS(External JS Solver) 챌린지 솔버가 필요하며, 없으면 비디오 포맷이 통째로 드랍(이미지만 남) 되거나 살아남아도 수백 KiB/s 로 심하게 스로틀됨. `BuildArgs`(다운로드)·`FetchFormatsAsync`(정밀 화질 드롭다운) 양쪽에 `--remote-components ejs:github`(yt-dlp 공식 권장, https://github.com/yt-dlp/yt-dlp/wiki/EJS) 추가. Deno 는 PATH 에 있으면 자동 감지(환경에 2.8.1 설치됨). 솔버 스크립트는 최초 1회만 GitHub 에서 받아 캐싱. (`Core/Social/YtDlpDownloader.cs`)
|
||||
- **정합성 주의**: `--remote-components` 는 yt-dlp 2025년 중반(EJS 도입) 이후 버전에서만 인식. 본 프로젝트는 yt-dlp 를 `releases/latest` 에서 자동 설치하므로 사실상 항상 지원; 단 사용자가 `YtDlpPath`(설정) 로 구버전을 수동 지정한 경우에만 "unrecognized arguments" 회귀 가능.
|
||||
|
||||
### 검증
|
||||
- 실제 동영상(`EjSqcVC1qwk`) 종단간 테스트: `--remote-components` 없으면 다운로드는 시작되나 `992KiB/s → Unknown B/s` 스로틀; 플래그 추가 시 `13~18 MiB/s` 정상 속도 + mp4 병합(EXIT=0). `-F` 포맷 정상 노출.
|
||||
- `dotnet build` 0 오류 / 0 경고.
|
||||
|
||||
## 6차 팔로업 (2026-07-29 완료) — YouTube 안티봇 실패 시 yt-dlp 자동 갱신/재시도
|
||||
> 사용자 요청: YouTube 가 난독화를 바꿀 때마다 수동 대응하지 않고 "자동으로 맞춰지게". yt-dlp 유지보수자가 보통 몇 시간~며칠 내 대응 릴리스를 내보내므로, 최신화만으로 대부분 복구됨에 착안.
|
||||
|
||||
### 기능
|
||||
- [x] **안티봇 실패 감지 → yt-dlp 최신화 → 1회 재시도** — `DownloadAsync` 에서 yt-dlp 종료코드≠0 일 때 stderr 에서 안티봇 징후(`n challenge`/`nsig`/`Only images are available`/`PO Token`/`po_token`/`Requested format is not available`/`Unable to extract`) 를 감지하면: (1) 설치 버전 vs GitHub Releases 최신 태그 비교, (2) 새 버전이 있을 때만 `releases/latest` 에서 강제 재다운로드(번들 경로) 후 동일 인자로 1회 재시도. 진행 중 카드에 "yt-dlp 갱신 확인 중 / x → y 갱신 후 재시도" 표시. (`Core/Social/YtDlpDownloader.cs`)
|
||||
- [x] **버전 비교로 불필요 재다운로드 방지** — 이미 최신인데도 실패하면 갱신 생략하고 에러 메시지에 "YouTube가 방금 바뀌었을 수 있음(잠시 후 재시도 권장)" 부연. (`Core/Social/YtDlpRunner.cs` — `GetVersionAsync`/`GetLatestVersionAsync`)
|
||||
- [x] **설정 토글** — `AppConfig.AutoUpdateYtDlp`(기본 true). 설정창 "yt-dlp 자동 다운로드" 옆 체크박스로 노출.
|
||||
|
||||
### 검증
|
||||
- 버전 형식 일치 확인: 설치 `2026.03.17` vs GitHub 최신 `2026.07.04` (둘 다 `YYYY.MM.DD` → 문자열 비교 정확). 현재 설치본이 최신보다 구버전이므로, 실패 시 `2026.07.04` 로 자동 갱신 후 재시도 동작 확인 가능.
|
||||
- 재시도 인자는 `--remote-components ejs:github` 포함(5차) 그대로 재사용 → EJS 솔버 + 바이너리 갱신 이중 방어.
|
||||
- `dotnet build`(전체 솔루션) 0 오류 / 0 경고.
|
||||
|
||||
## 7차 팔로업 (2026-07-29 완료) — Deno(JS 런타임) 자동 설치로 배포 환경 EJS 보장
|
||||
> 사용자 요청(연장): "Deno도 없으면 자동 설치". 5차의 `--remote-components ejs:github` 는 JS 런타임(Deno) 이 있어야 동작 — 개발 머신엔 Deno 가 있지만 배포 PC엔 없으면 EJS 가 안 풀려 YouTube 가 막힘.
|
||||
|
||||
### 기능
|
||||
- [x] **DenoRunner 자동 설치** — `Resolve`(설정→번들→PATH)·`EnsureInstalledAsync`·`DownloadAsync`(GitHub `denoland/deno` Releases zip → `deno.exe` 단일 엔트리 추출). yt-dlp 와 동일 `%AppData%/VideoDownloader/tools` 공유. OS/아키텍처별 에셋(Windows x64 / macOS·Linux arm64+x64). (`Core/Social/DenoRunner.cs`)
|
||||
- [x] **PATH 주입(플래그 의존 제거)** — `YtDlpRunner.RunAsync`/`RunCaptureAsync` 가 yt-dlp 자식 프로세스 PATH 앞에 ToolsDir 를 추가 → 번들 `deno.exe` 를 yt-dlp 가 자동 발견. `--js-runtimes deno:<경로>` 플래그의 Windows 경로 콜론 파싱 엣지케이스를 피하는 가장 견고한 방식(시스템 PATH 불변, 자식 프로세스에만 적용). (`Core/Social/YtDlpRunner.cs`)
|
||||
- [x] **연결** — `DownloadAsync` 시작 시 Deno 를 ensure(best-effort, 진행률 표시). Deno 누락은 치명 오류가 아니므로(yt-dlp 가 경고 후 진행) 결과 무시. `AppConfig.DenoPath`·`AutoInstallDeno`(기본 true) + 설정 토글.
|
||||
- **3중 방어 완성**: (1) EJS 솔버 `--remote-components ejs:github`(5차) + (2) yt-dlp 자동 갱신(6차) + (3) Deno 자동 설치(7차).
|
||||
|
||||
### 검증
|
||||
- **Deno 없는 환경 시뮬레이션 종단 테스트**: 시스템 deno(npm) 를 PATH에서 제외하고 번들 `deno.exe`(/tmp/vdtools) 만 노출한 상태에서 `yt-dlp --remote-components ejs:github` 실행 → `which deno` = 번들만, 다운로드 **15~124 MiB/s 풀스피드 + 경고 0건 + EXIT=0**(mp4 병합). PATH 주입으로 번들 Deno 가 n 챌린지 해석 확인.
|
||||
- Deno 다운로드/추출: zip(42MB) → `deno.exe`(97MB) 단일 파일, v2.9.4 정상 동작 확인.
|
||||
- `dotnet build`(전체 솔루션) 0 오류 / 0 경고.
|
||||
|
||||
## 8차 팔로업 (2026-07-29 완료) — 배포 파이프라인 + Avalonia 크로스플랫폼 이관 착수
|
||||
> 사용자 요청: "배포도 신경써보자" — Windows 다중 아키텍처 정식 설치자 + Avalonia UI 재작성 착수(맥/리눅스 지원 목표). 계획 승인 후 2단계로 실행.
|
||||
|
||||
### Phase 1 — Windows Inno Setup 다중 아키텍처 설치자 (완전 완료)
|
||||
- [x] **`VideoDownloader.App.csproj` 멀티아키텍처/self-contained** — `<Platforms>x64;arm64</Platforms>`, `<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>`, `SelfContained=true`(폴더, single-file/trim 아님 — WPF+트리밍 위험 회피). `PlatformTarget` 하드코딩 제거(RID에서 파생).
|
||||
- [x] **`build/publish.ps1`** — x64/arm64 루프: `dotnet publish -c Release -r <rid> -p:Platform=<arch> --self-contained` 후 ISCC(`/DARCH=`)로 `out/VideoDownloader-Setup-<arch>.exe` 생성. ISCC 위치 자동 해결(기본경로/PATH/LocalAppData 후보).
|
||||
- [x] **`build/installer.iss`** — 단일 스크립트 2회 컴파일. 아키텍처별 고유 AppId GUID; `ArchitecturesAllowed` 게이트(arm64 전용 / x64=x64compatible+arm64폴백); per-user 설치(`PrivilegesRequired=lowest`); 시작메뉴+데스크톱 바로가기+제거자; 한국어 기본(`Korean.isl`)+영어(`Default.isl`).
|
||||
- **Inno Setup**: winget `JRSoftware.InnoSetup`(6.7.3, per-user) 설치. 영어는 `Languages/English.isl`이 아니라 루트 `Default.isl`이 기본(트랩).
|
||||
|
||||
### Phase 1 검증
|
||||
- **두 설치자 생성**: `out/VideoDownloader-Setup-x64.exe`(49.1 MB), `out/VideoDownloader-Setup-arm64.exe`(43.8 MB). 둘 다 유효 PE(MZ).
|
||||
- **arm64 publish 성공** — WPF arm64(self-contained) 첫 빌드, 이슈 없음(예상 리스크 미발현). yt-dlp 네이티브 arm64 빌드가 없어 arm64 Win11에선 x64 에뮬레이션(WoW64)으로 동작(릴리스 노트 명시 필요).
|
||||
- `publish.ps1` exit 0(두 아키텍처 publish+ISCC 전 단계).
|
||||
|
||||
### Phase 2 — Avalonia 크로스플랫폼 UI (기반 + Gate #0, 로드맵 포함)
|
||||
- [x] **`VideoDownloader.Avalonia`(신규, net8.0)** — WPF 앱은 유지한 채 병행. Core 참조(엔진 공유). `VideoDownloader.slnx` 등록.
|
||||
- [x] **Gate #0 최소 동작 UI** — `MainWindow.axaml`/`MainViewModel`(INPC) + `RelayCommand`. URL 붙여넣기 → 유형 판별(소셜/HLS/DASH/직링크) → Core 다운로더 **직접** 호출(DownloadManager의 WebView2 쿠키 결합 회피), 진행률/로그 표시. 소셜은 방금 만든 자동 복구(6차)·Deno 자동설치(7차) 로직도 그대로 작동.
|
||||
- **Gate #1(관문) = 크로스플랫폼 브라우저 컴포넌트**: 현재 앱의 심장은 WebView2의 응답 가로채기+쿠키. 추천 = **CefGlue.Avalonia**(`OnResourceResponse`↔`WebResourceResponseReceived`, `CefCookieManager`↔쿠키, 거의 1:1). 단, CEF가 자체 Chromium 번들(~150-200MB/OS) + 성숙도/Linux 리스크 → **3OS 프로토타입 통과 전 본 이관 착수 금지**(로드맵 1순위). 차선 CefNet, 최후 DotNetBrowser(상용).
|
||||
|
||||
### Phase 2 검증
|
||||
- `dotnet build` 0 오류/0 경고(Avalonia 12.1.0 + Core net8.0). (컴파일드 바인딩 → 코드비하인드 DataContext이므로 `x:CompileBindings="False"`; `Items`→`ItemsSource`; `Watermark`→`PlaceholderText`.)
|
||||
- **스모크 테스트**: `dotnet run` → 창 정상 시작, 10초 생존(크래시 없음, 빈 로그) → 크로스플랫폼 빌드 + Core 재사용 + MVVM 바인딩 입증.
|
||||
|
||||
### 이관 로드맵(의존 순)
|
||||
1. Gate #1 브라우저 3OS 프로토타입(CEF) — 통과 전 3~8 불가.
|
||||
2. 공유 리팩터: `App/Detection/*`(순수C#) Core 이동; `ICookieProvider` 중립 추상화; `DownloadManager` 공유화(생성자→`ICookieProvider`); `BindingOperations.EnableCollectionSynchronization`→Avalonia 디스패처; `IAppPaths` OS별 경로.
|
||||
3~8. 브라우저+감지, 설정, 테마(Avalonia Styles 재작성), 보더리스 윈도우(`ExtendClientAreaToDecorationsHint`), ViewModels, 북마크/히스토리.
|
||||
9. Mac/Linux 패키징(`.app` 노타라이제이션, `.AppImage`/`.deb`); `YtDlpRunner`/`DenoRunner` OS/arch별 바이너리 확인. 10. 패리티 후 WPF 은퇴.
|
||||
- **참고**: 저장소가 git 아님 → Phase 2 병행 작업 격리 위해 `git init` 권장.
|
||||
262
docs/BROWSER_FEATURES_PLAN.md
Normal file
262
docs/BROWSER_FEATURES_PLAN.md
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
# Video Downloader — 브라우저 기능 강화 계획서 (v2)
|
||||
|
||||
> **목표**: 이 앱에 내장된 WebView2 브라우저에 일반적인 데스크톱 브라우저의 **핵심 사용자 기능**을 심어, 단순 "영상 재생 → 감지" 용도를 넘어 **일상적으로 쓸 수 있는 브라우저**로 만든다.
|
||||
>
|
||||
> **작성일**: 2026-08-15 (v2 — 2차 심층 조사 반영) · **대상**: `VideoDownloader.App` (WPF + WebView2 1.0.4078.44)
|
||||
>
|
||||
> **조사 방법론**:
|
||||
> - 1차(2026-08-13): Chrome/Firefox/Zen 3개 브라우저 단일 조사 → v1 초안
|
||||
> - **2차(2026-08-15): 5개 전문 조사팀 병렬 심층 조사** — ① Chrome+Edge ② Firefox+Vivaldi ③ Zen+Arc ④ Brave+Opera+Safari+기타(Floorp/Orion/Sidekick/Comet) ⑤ **WebView2 API 검증팀(Microsoft Learn 팩트체크)**. 총 80여 회 웹 검색 + 공식 문서 직접 열람.
|
||||
>
|
||||
> **확장 계획**: 모바일 앱 + PC↔모바일 연동은 **`MOBILE_PLAN.md`** 로 분리 (2026-08-15, 4개 조사팀 심층 조사 기반 — MAUI 10 컴패니언 앱, LAN QR 페어링, Core 엔진 재사용 검증 포함). 멀티플랫폼 배포(Win/macOS/Linux/모바일)는 **`DEPLOYMENT_PLAN.md`** 로 분리.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ v1 → v2 주요 수정 사항 (2차 조사로 판명)
|
||||
|
||||
1. **페이지 내 검색은 네이티브 API로 가능** — v1은 "JS 주입으로 직접 구현"이라 판단했으나 **WebView2 1.0.3405.78부터 `CoreWebView2.Find`가 네이티브 제공**된다 (우리 버전 1.0.4078.44에 포함). `StartAsync(options)` + `FindNext()/FindPrevious()` + `MatchCount`/`ActiveMatchIndex` 이벤트. `SuppressDefaultFindDialog=true`로 자체 UI 구축. → **공수 대폭 감소, 탭 이전에도 독립 구현 가능**
|
||||
2. **백그라운드 탭 절전도 네이티브** — `CoreWebView2.TrySuspendAsync()`/`Resume()` (WebView가 숨겨진 상태여야 함). Edge Sleeping Tabs와 1:1 대응.
|
||||
3. **탭 오디오 표시/음소거 네이티브** — `IsDocumentPlayingAudio`/`IsMuted` + 변경 이벤트.
|
||||
4. **다중 프로필이 하나의 환경에서 가능** — `CreateCoreWebView2ControllerOptions(ProfileName)` → 브라우저 프로세스 추가 없이 Firefox 컨테이너 탭 유사 격리. `CustomDataPartitionId`로 탭 단위 쿠키 격리도 가능.
|
||||
5. **Arc 브라우저는 2025-05 개발 중단** (유지보수 모드) — 디자인 참고용으로만 차용.
|
||||
6. **Edge Collections/Drop/사이드바는 2026년 폐지 예정** — 클론하지 않음. 살아남는 Edge 자산은 세로 탭·절전 탭·분할 화면·웹 캡처·몰입형 리더.
|
||||
7. **WPF airspace 주의** — 표준 `WebView2`(HwndHost) 위에는 WPF 요소를 올릴 수 없음. `WebView2CompositionControl`은 DRM 영상 재생 불가(영상 앱엔 치명적) → **표준 컨트롤 유지, 크롬 UI는 WebView2 주변에 배치**하는 설계 원칙 확정.
|
||||
|
||||
---
|
||||
|
||||
## 0. 현재 상태 (AS-IS)
|
||||
|
||||
`MainWindow.xaml(.cs)` 단일 `WebView2` 인스턴스 기반:
|
||||
|
||||
| 영역 | 구현 여부 | 비고 |
|
||||
|---|---|---|
|
||||
| 주소창 + 이동 + 자동완성/제안 | ✅ | 히스토리 기반, 인라인 자동완성(Tab/→) |
|
||||
| 뒤로/앞으로/새로고침/홈 | ✅ | 상태 동기화 |
|
||||
| 북마크 / 히스토리 | ✅ | 플라이아웃, JSON 영속(500 캡) |
|
||||
| InPrivate 모드 | ✅ | 재시작 적용 |
|
||||
| 단축키 | ✅(일부) | Ctrl+L, F5/Ctrl+R, Esc, Alt+←/→, Ctrl+±/0 |
|
||||
| 줌 | ✅ | 세션 한시적(사이트별 영속 아님) |
|
||||
| 팝업 → 현재 WebView 라우팅 | ✅ | `NewWindowRequested` |
|
||||
| 페이지 다운로드 → 저장 폴더 | ✅ | `DownloadStarting` |
|
||||
| 영구 프로필 / 데이터 삭제 | ✅ | `%AppData%/VideoDownloader/webview` |
|
||||
| 다크/라이트 테마, 보더리스 | ✅ | DWM 연동 |
|
||||
| 우측 다운로드 패널(접기/폭) | ✅ | |
|
||||
| DevTools, 미디어 자동 감지 | ✅ | HLS/DASH/직링크/소셜(yt-dlp) |
|
||||
|
||||
**결여**: 탭 · 페이지 내 검색 · 다운로드 매니저 UI · 검색엔진 · 북마크바 · 명령 팔레트 · 리더 모드 · 스크린샷 · 전체화면 · 사이트별 설정
|
||||
|
||||
---
|
||||
|
||||
## 1. WebView2 API 역량 매트릭스 (2차 조사 검증 완료)
|
||||
|
||||
> 우리 버전(1.0.4078.44) 기준. 구현 계획의 기술적 토대.
|
||||
|
||||
| 필요 기능 | 네이티브 API | 비고 |
|
||||
|---|---|---|
|
||||
| **페이지 내 검색** | `CoreWebView2.Find` → `StartAsync(CoreWebView2FindOptions)` / `FindNext()` / `FindPrevious()` / `Stop()`, `MatchCountChanged`·`ActiveMatchIndexChanged` 이벤트 | 대소문자·단어일치·전체 강조 옵션 내장. `SuppressDefaultFindDialog=true` 필수(자체 UI) |
|
||||
| **다중 탭** | 공유 `CoreWebView2Environment` + N개 `WebView2` 컨트롤 | 인스턴스당 메모리 부담 → 절전과 짝꿍. 브라우저 프로세스 1개 공유 |
|
||||
| **탭 절전** | `TrySuspendAsync()` / `Resume()` / `IsSuspended` / `MemoryUsageTargetLevel` | 숨겨진(비활성) 탭에서만 동작. 자동으로 Low 모드 진입 |
|
||||
| **오디오** | `IsMuted`(읽기/쓰기) · `IsDocumentPlayingAudio` · 변경 이벤트 | 음량 조절은 불가(뮤트만). 초기화 전 뮤트 설정은 안 먹는 버그(#2120) |
|
||||
| **컨테이너/프로필** | `CreateCoreWebView2ControllerOptions(ProfileName, IsInPrivateModeEnabled)`, `CoreWebView2Profile`, `CustomDataPartitionId` | 한 환경에서 여러 프로필 = 프로세스 추가 없이 쿠키/세션 격리 |
|
||||
| **다운로드 관리** | `DownloadStarting` + `CoreWebView2DownloadOperation`: `Pause()/Resume()/Cancel()`, `BytesReceived`/`TotalBytesToReceive`/`State`/`InterruptReason`, 이벤트 3종 | 속도는 BytesReceived 델타로 자체 계산 |
|
||||
| **팝업→새 탭** | `NewWindowRequested`: `e.NewWindow = 새탭.CoreWebView2`, `GetDeferral()` | 탭 생성이 비동기라 deferral 필수 |
|
||||
| **줌** | `Controller.ZoomFactor` + `ZoomFactorChanged` | 컨트롤러(탭) 단위. **사이트별 영속은 WebView2이 보장 안 함** → 앱 설정에 도메인→배율 저장 후 `NavigationCompleted` 때 재적용 |
|
||||
| **파비콘/제목** | `DocumentTitle(Changed)`, `FaviconUri(Changed)`, `GetFaviconAsync(Png)` | 탭 UI 재료 |
|
||||
| **컨텍스트 메뉴** | `ContextMenuRequested` + `CreateContextMenuItem` | 기본 메뉴 항목 재구성 + 커스텀 항목(「이 링크의 영상 받기」 등) |
|
||||
| **권한** | `PermissionRequested` + `Profile.SetPermissionStateAsync(kind, origin, state)` | 알림/카메라/마이크/자동재생 등 사이트별 영속 |
|
||||
| **쿠키** | `CookieManager`(Profile 스코프) | |
|
||||
| **인쇄/PDF** | `ShowPrintUI()`, `PrintAsync(settings)`, `PrintToPdfAsync(path)` | |
|
||||
| **C#↔JS 브리지** | `PostWebMessageAsJson` ↔ `WebMessageReceived`, `AddScriptToExecuteOnDocumentCreatedAsync` | 리더 모드/부스트/요소 zap의 기반. CSP에 막히지 않음(단 Trusted Types DOM 싱크 주의) |
|
||||
| **알림** | `NotificationReceived` (비영속 알림만, 호스트가 직접 표시) | 푸시(서비스워커) 알림은 미지원 |
|
||||
| **스크린샷** | `CapturePreviewAsync`(뷰포트만) / CDP `Page.captureScreenshot`(`captureBeyondViewport:true`로 전체 페이지) | 전체 페이지 캡처는 CDP 경유 |
|
||||
| **오류 페이지** | `IsBuiltInErrorPageEnabled=false` + 실패 시 `NavigateToString` | 재진입 가드 필요(#634) |
|
||||
| **상태바 링크 미리보기** | `StatusBarText(Changed)` | 링크 호버 시 하단 표시 |
|
||||
|
||||
**자체 구현 필요** (네이티브 없음): 리더 모드(Readability.js 주입) · 검색엔진 로직(순수 호스트) · 히스토리/북마크 저장(이미 보유) · 명령 팔레트 · 탭 그룹/워크스페이스(UI 개념)
|
||||
|
||||
---
|
||||
|
||||
## 2. 조사 결과 종합 — 9개 브라우저의 차별 기능
|
||||
|
||||
### Chrome (표준의 척도)
|
||||
탭 그룹(색+이름+접기, **저장된 그룹**), Memory Saver(유휴 탭 동결), **탭 검색 Ctrl+Shift+A**, `@tabs`/`@bookmarks`/`@history` 주소창 스코프 검색, 키워드 사이트 검색(`wiki`+Tab), Ctrl+Shift+T 복원, 다운로드 버블(진행 링), 사이트별 줌 영속, 빠른 닫힘. 2025+ : 세로 탭 추가, AI 탭 정리.
|
||||
|
||||
### Edge (WebView2의 친척 — 가장 직접 참고)
|
||||
**세로 탭**(좌측 사이드바, 아이콘 레일 축소+호버 확장), **Sleeping Tabs**(30초~12h 설정, "zzz" 표시, 절약량 표시), **네이티브 분할 화면**, **웹 캡처**(영역/전체 페이지+주석, Ctrl+Shift+S), **몰입형 리더**(읽어주기 Ctrl+Shift+U, 문법 도구), 즐겨찾기바 "아이콘만" 모드, F4=주소창. (Collections/Drop/사이드바는 2026 폐지 — 미차용)
|
||||
|
||||
### Firefox
|
||||
**Reader View(F9)** — 폰트/간격/테마(밝/어/세피아/대비) + **낭독(TTS)**; **Multi-Account Containers**(쿠키 격리 다중 로그인); **Enhanced Tracking Protection**(실드 아이콘, 3단계); HTTPS-Only; **빠른 찾기** `/`(즉시), `'`(링크만); **PiP**(자동 열림 옵션); 다운로드 패널(.part 재개).
|
||||
|
||||
### Vivaldi (커스터마이징의 왕)
|
||||
**Quick Commands(F2)** — 전 브라우저 액션 명령 팔레트; **Tab Stacks**(드래그 스택, Compact/2단/아코디언 표시); **Tab Tiling**(수직/수평/그리드, Ctrl+F9/F8/F7); **Workspaces + 규칙**(URL 패턴→워크스페이스 자동 라우팅!); **북마크 닉네임**(`g 검색어`); **Speed Dial**(새 탭 썸네일 그리드); **Web Panels**(사이드바 고정 사이트); **Notes**(마크다운, "Add to Note"); **Page Actions**(CSS 필터: 흑백/반전/세피아/가리개); **Capture**(전체/영역); 주소창 진행 바, 파라미터 제거 복사, 마우스 제스처, 툴바 편집.
|
||||
|
||||
### Zen Browser (Firefox 포크, 활발)
|
||||
**세로 탭 철학** + **Essentials**(전 워크스페이스 상시 아이콘 고정); **Workspaces**(탭 세트 분리); **Split View**(최대 4분할); **Glance**(Alt+클릭 링크 미리보기 플로팅, 닫기/탭으로/분할 버튼); **Compact Mode**(사이드바 숨김+엣지 호버 복원); **Boosts**(사이트별: 색 틴트/폰트/요소 zap/강제 다크); 핀 탭 닫으면 언로드(삭제 아님); Mods(CSS+JS 마켓).
|
||||
|
||||
### Arc (개발 중단 — 개념만 차용)
|
||||
**Command Bar(Ctrl+T 스포트라이트)** — 탭/북마크/히스토리/명령/웹검색 통합; **Spaces**(테마별 공간); **Auto Archive**(미고정 탭 12h 후 아카이브, 닫기=아카이브, 검색 가능한 복구 목록); **Boosts**(슬라이더: 색/대비/밝기/채도/세피아/다크/줌 + CSS); **Peek**(Shift+클릭 미리보기); **Air Traffic Control**(도메인→Space 자동 라우팅); URL 바 **탭별 입력 초안 기억**.
|
||||
|
||||
### Brave
|
||||
**Playlist** ★영상 앱에 최적 — 주소창 "미디어 추가" 버튼으로 페이지 영상/오디오를 재생목록에 저장, 백그라운드 재생, 광고 없는 재생, 오프라인 저장; Shields(사이트별 차단 슬라이더 + 차단 카운터); Speedreader(자동 리더).
|
||||
|
||||
### Opera
|
||||
**Tab Islands**(같은 부모에서 연 탭 자동 그룹); **Video Popout**(호버 버튼→분리 플레이어); 사이드바 메신저; 무료 VPN; Speed Dial; Workspaces.
|
||||
|
||||
### Safari / 기타
|
||||
Safari: Tab Groups + Reading List(오프라인 나중읽기), 컴팩트 탭바. Floorp: 멀티로우 탭. Orion: Lazy Load(포커스 시 로드). (Sidekick 2025 종료, Comet/Dia = AI 브라우저 흐름 참고)
|
||||
|
||||
---
|
||||
|
||||
## 3. 도입 기능 목록 (v2 — 재서열)
|
||||
|
||||
### 🏆 Tier 1 — 필수 (일반 브라우저의 기본기)
|
||||
| # | 기능 | 근원 | 구현 경로 | 공수 |
|
||||
|---|---|---|---|---|
|
||||
| T1-1 | **탭 시스템** (생성/닫기/전환/Ctrl+Tab·1~9/드래그 정렬/복제) | 전 브라우저 | 공유 Environment + 탭당 WebView2 + `NewWindowRequested`→새 탭(deferral) | L |
|
||||
| T1-2 | **닫은 탭 복원** Ctrl+Shift+T | 전 브라우저 | 닫힘 스택(인덱스+URL 보존) | S |
|
||||
| T1-3 | **페이지 내 검색** Ctrl+F (일치 수 X/Y, 다음/이전, 대소문자) | 전 브라우저 | **네이티브 `CoreWebView2.Find`** + 자체 하단 바 | **S** ↓↓ |
|
||||
| T1-4 | **검색엔진** (기본 엔진 선택 + 키워드 전환 `g `, `yt ` + `@북마크/@기록` 로컬 스코프) | Chrome/Vivaldi/Firefox | 순수 호스트 로직 + 제안 드롭다운 확장 | S~M |
|
||||
| T1-5 | **다운로드 매니저 UI** (진행+속도, 일시정지/재개/취소, 폴더 열기, Ctrl+J) | Chrome 버블/Vivaldi 패널 | `CoreWebView2DownloadOperation` 풀 API + 앱 카드와 통합 뷰 | M |
|
||||
| T1-6 | **북마크바** (주소창 아래, Ctrl+Shift+B, 아이콘만 모드) | Chrome/Edge | UI 행 | S |
|
||||
| T1-7 | **사이트별 줌 영속** | 전 브라우저 | 도메인→배율 사전 저장 + `NavigationCompleted` 재적용 | S |
|
||||
| T1-8 | **탭 오디오 표시 + 클릭 음소거** | Chrome/Edge | `IsDocumentPlayingAudio`/`IsMuted` 네이티브 | S |
|
||||
| T1-9 | **강제 새로고침** Ctrl+Shift+R | 전 브라우저 | `Profile.ClearBrowsingDataAsync(DiskCache)` 후 Reload | S |
|
||||
| T1-10 | **세션 복원** (시작 시 이전 탭 복구, 옵션) | Chrome/Firefox | 탭 URL 리스트 영속 | S |
|
||||
| T1-11 | **전체화면 F11** + 비디오 전체화면 지원 | 전 브라우저 | `ContainsFullScreenElement` 이벤트 + 크롬 자동 숨김 | S |
|
||||
|
||||
### 🥈 Tier 2 — 강력 추천 (차별화 + 사용성 대폭 향상)
|
||||
| # | 기능 | 근원 | 구현 경로 | 공수 |
|
||||
|---|---|---|---|---|
|
||||
| T2-1 | **명령 팔레트** (Ctrl+K: 탭/북마크/기록/액션/웹검색 통합) | Arc Command Bar / Vivaldi Quick Commands | 호스트 UI + 기존 데이터 재사용 | M |
|
||||
| T2-2 | **탭 절전** (유휴 탭 자동 서스펜드, "zzz" 표시, 예외 사이트) | Edge Sleeping Tabs / Zen | `TrySuspendAsync`/`Resume` + 유휴 타이머 | M |
|
||||
| T2-3 | **분할 화면** (2~4분할, 드래그 배치, 분할 닫기) | Zen Split / Edge / Vivaldi Tiling | Grid + GridSplitter + WebView2 복수 | M~L |
|
||||
| T2-4 | **리더 모드** (F9, 본문 추출 + 폰트/테마) | Firefox/Zen | Readability.js 주입 + 스타일 오버레이 | M |
|
||||
| T2-5 | **세로 탭 레이아웃** (좌측 사이드바, 축소 레일, 토글) | Edge/Zen | 탭 스트립 위치 전환 | M |
|
||||
| T2-6 | **Glance 링크 미리보기** (Alt+클릭 → 플로팅 미리보기, 탭으로 승격 버튼) | Zen Glance / Arc Peek | 오버레이 WebView2 창 | M |
|
||||
| T2-7 | **사이트별 Boost** (강제 다크 / 색 틴트 / 요소 zap / 줌 기억) | Zen·Arc Boosts | 도메인별 CSS 주입 `AddScriptToExecuteOnDocumentCreated` | M |
|
||||
| T2-8 | **탭 검색** Ctrl+Shift+A (열린+최근 닫힘 탭 검색 점프) | Chrome/Edge | 호스트 UI | S |
|
||||
| T2-9 | **스크린샷** (보이는 영역 / 전체 페이지, Ctrl+Shift+S) | Firefox/Edge/Vivaldi | `CapturePreviewAsync` / CDP `captureBeyondViewport` | S~M |
|
||||
| T2-10 | **미디어 재생목록** ★영상 앱 핵심 — 감지된 영상을 재생목록에 쌓고 앱 내 재생/백그라운드 | Brave Playlist | 감지 파이프라인 + 내장 플레이어 컨트롤 | L |
|
||||
| T2-11 | **탭 그룹** (색+이름, 접기, 도메인별 자동 그룹) | Chrome 탭 그룹 / Opera Islands | 탭 모델 확장 | M |
|
||||
| T2-12 | **웹 캡처 → 다운로드 큐 연동** (우클릭 "이 링크 영상 받기") | 커스텀 | `ContextMenuRequested` 커스텀 항목 | S |
|
||||
|
||||
### 🥉 Tier 3 — 선택 (돋보이는 기능)
|
||||
| # | 기능 | 근원 | 비고 |
|
||||
|---|---|---|---|
|
||||
| T3-1 | **워크스페이스** + **도메인 라우팅 규칙** ("youtube.com → 영상 공간") | Zen/Vivaldi 규칙/Arc ATC | 탭 세트 스왑 |
|
||||
| T3-2 | **Auto-Archive 탭 수명주기** (닫기=아카이브, 캐던스 설정, 복구 패널) | Arc | 히스토리와 통합 |
|
||||
| T3-3 | **컨테이너/다중 프로필** (탭별 쿠키 격리 다중 로그인) | Firefox/Zen | `ProfileName`/`CustomDataPartitionId` |
|
||||
| T3-4 | **빠른 찾기** `/` · `'`(링크만) | Firefox | Find API 래핑 |
|
||||
| T3-5 | **컨텍스트 메뉴 전면 커스터마이징** | — | `ContextMenuRequested` (T2-12 확장) |
|
||||
| T3-6 | **Page Actions** (CSS 필터: 흑백/반전=다크/세피아/가리개) | Vivaldi | Boost와 통합 가능 |
|
||||
| T3-7 | **PiP 영상 팝아웃** (호버 버튼 → 항상 위 창) | Firefox/Opera | Document PiP API or 별도 창 |
|
||||
| T3-8 | **Speed Dial 새 탭 페이지** (썸네일 그리드) | Opera/Vivaldi | 내장 HTML |
|
||||
| T3-9 | **읽어주기(TTS)** | Edge/Firefox | 리더 모드와 세트, Windows TTS |
|
||||
| T3-10 | **인쇄/PDF** Ctrl+P / 소스보기 Ctrl+U | 전 브라우저 | `PrintToPdfAsync` 등 네이티브 |
|
||||
| T3-11 | **권한 UI** (알림/카메라/마이크/자동재생 사이트별) | Chrome/Firefox | `PermissionRequested` |
|
||||
| T3-12 | **노트 패널** (마크다운, "Add to Note") | Vivaldi/Arc | 후순위 |
|
||||
| T3-13 | **컴팩트 모드** (크롬 전체 숨김, 엣지 호버 복원) | Zen/Arc | 전체화면과 통합 |
|
||||
| T3-14 | **추적 방지 표시** (차단 카운터 실드) | Firefox/Brave | `WebResourceRequested` 필터 |
|
||||
| T3-15 | **주소창 진행 바 + 파라미터 제거 복사** | Vivaldi | 소소한 완성도 |
|
||||
|
||||
**❌ 의도적 미차용**: 비밀번호 관리(자동저장 토글만), 실제 확장 프로그램 생태계, 클라우드 동기화, AI 사이드바(방향 불명확), Edge Collections/Drop(폐지), VPN/Tor(범위 외)
|
||||
|
||||
---
|
||||
|
||||
## 4. 구현 로드맵 (의존성 순, v2)
|
||||
|
||||
### Phase 0 — 즉효 퀵윈 (탭 이전 독립 구현 가능, 반나절~1일)
|
||||
> 신규 발견: 네이티브 API 덕에 탭 대공사 없이 먼저 넣을 수 있는 것들.
|
||||
- [ ] **T1-3 페이지 내 검색** — `CoreWebView2.Find` + 하단 검색 바(Ctrl+F/F3/Esc). *가성비 1위로 승격*
|
||||
- [ ] **T1-4 검색엔진** — URL 판별 → 검색 URL 조합, 기본 엔진 설정(Google/Bing/DDG), `g ` 키워드
|
||||
- [ ] **T1-7 사이트별 줌 영속**
|
||||
- [ ] **T1-9 강제 새로고침**
|
||||
- [ ] **T1-11 전체화면 F11**
|
||||
|
||||
### Phase A — 탭 아키텍처 (대공사, 모든 탭 기능의 전제)
|
||||
- [ ] `Browser/TabViewModel` + `TabManager` 신규 — 공유 `CoreWebView2Environment`, 탭당 WebView2
|
||||
- [ ] 탭 스트립 UI(가로 우선, 세로는 T2-5) + 활성 탭만 Visible
|
||||
- [ ] 기존 로직 전면 리팩터: 주소창/네비/감지(`OnResponseReceived` 전 탭 구독→중계)/북마크/히스토리/DetectSocial → 활성 탭 대상
|
||||
- [ ] 단축키: Ctrl+T/W/Tab/1~9, Ctrl+Shift+T(T1-2 복원 스택)
|
||||
- [ ] `NewWindowRequested` → 새 탭(deferral 패턴) 교체
|
||||
- [ ] T1-8 오디오 표시/뮤트, T1-10 세션 복원
|
||||
- ⚠️ 저장소가 git이 아님 → **착수 전 `git init` 필수**. 감지 로직 회귀 테스트 체크리스트 작성.
|
||||
|
||||
### Phase B — 탭 보조 + 핵심 완성
|
||||
- [ ] **T1-5 다운로드 매니저** — 페이지 다운로드(`DownloadOperation`) + 영상 카드 통합 뷰, 속도/일시정지/재개
|
||||
- [ ] **T1-6 북마크바** (+아이콘만 모드)
|
||||
- [ ] **T2-2 탭 절전** — 유휴 타이머 → `TrySuspendAsync`, 재활성 `Resume`
|
||||
- [ ] **T2-8 탭 검색** Ctrl+Shift+A
|
||||
- [ ] **T2-12 우클릭 "이 링크 영상 받기"** — 컨텍스트 메뉴 커스텀 (다운로더 앱의 시그니처)
|
||||
|
||||
### Phase C — 차별화 (Zen/Arc/Vivaldi 영감)
|
||||
- [ ] **T2-1 명령 팔레트** Ctrl+K — 탭/북마크/기록/액션(다운로드·줌·테마·InPrivate)/웹검색
|
||||
- [ ] **T2-3 분할 화면** — 플레이어 + 다운로드 큐 용도
|
||||
- [ ] **T2-4 리더 모드** — Readability.js 번들
|
||||
- [ ] **T2-6 Glance** Alt+클릭 미리보기
|
||||
- [ ] **T2-7 Boost** — 강제 다크/틴트/요소 zap(사이트별 광고 제거)
|
||||
- [ ] **T2-9 스크린샷**, **T2-5 세로 탭**, **T2-11 탭 그룹**
|
||||
|
||||
### Phase D — 시그니처 + 정제
|
||||
- [ ] **T2-10 미디어 재생목록** ★ — 감지된 영상 → 재생목록 패널 → 내장 재생/백그라운드 (Brave Playlist 관점, 우리 앱의 정체성)
|
||||
- [ ] T3-1 워크스페이스+도메인 라우팅, T3-2 Auto-Archive, T3-3 컨테이너, T3-7 PiP, T3-8 Speed Dial 등
|
||||
|
||||
---
|
||||
|
||||
## 5. 우선순위 TOP 5 (v2 — 공수 재평가 반영)
|
||||
|
||||
| 순위 | 기능 | 이유 |
|
||||
|---|---|---|
|
||||
| 1 | **Phase 0 전체** (검색·검색엔진·줌·F11) | 탭 없이도 즉시 체감. 특히 페이지 내 검색은 네이티브 API로 반나절. |
|
||||
| 2 | **Phase A 탭** | 브라우저 정체성의 기반. 이후 모든 것의 전제. |
|
||||
| 3 | **다운로드 매니저 (T1-5)** | 다운로더 앱인데 관리 UI 부재가 최대 모순. |
|
||||
| 4 | **명령 팔레트 (T2-1)** | 모든 기능의 통합 입구 — 기능이 많아질수록 가치 증가. |
|
||||
| 5 | **미디어 재생목록 (T2-10)** | Brave Playlist 관점 — "영상 다운로더 브라우저"의 시그니처 기능. |
|
||||
|
||||
---
|
||||
|
||||
## 6. 기술 노트 & 리스크 (2차 검증 반영)
|
||||
|
||||
- **airspace**: 표준 `WebView2`(HwndHost)는 최상위 렌더 → WPF 오버레이 불가. `WebView2CompositionControl`은 DRM 영상 실패 → **불가침 원칙: 크롬 UI는 WebView2를 덮지 않고 둘러싼다**. (Glance/커맨드 팔레트는 별도 창 or 여백 배치)
|
||||
- **탭 메모리**: 인스턴스당 수십 MB. → 절전(T2-2)을 Phase B에 배치, 최대 탭 수 가드(옵션), Zen식 "핀 탭 닫으면 언로드" 채택 고려.
|
||||
- **줌 영속**: WebView2이 사이트별 줌을 보장하지 않음(#2451/#3459) → 앱 설정 사전이 SSOT.
|
||||
- **Find API**: 옵션 변경 시 재 Start. HTML/텍스트 문서만. `SuppressDefaultFindDialog=true`로 Edge 기본 바 차단.
|
||||
- **뮤트 버그**: 첫 네비게이션 전 `IsMuted=true`는 무시될 수 있음(#2120) → 초기화 후 설정.
|
||||
- **ClearBrowsingDataAsync 셧다운**: 앱 종료 중 호출 미완료 이슈(#3176) → 종료 경로에서 호출 금지.
|
||||
- **Trusted Types**: 일부 사이트에서 `innerHTML` 주입이 막힘 → `textContent`/`createElement` 사용.
|
||||
- **미디어 감지 다중화**: `WebResourceResponseReceived`를 탭마다 구독하되 `TabManager`가 출처 태깅 후 `DownloadManager.OnDetected` 중계 (카드에 탭 출처 표시).
|
||||
|
||||
---
|
||||
|
||||
## 7. 성공 기준 (완료 정의)
|
||||
|
||||
- [ ] Ctrl+F 검색 바: 일치 수 표시, 다음/이전, Esc 닫기 동작
|
||||
- [ ] 주소창에 검색어 → 선택한 엔진으로 검색; `g `/`yt ` 키워드 전환 동작
|
||||
- [ ] 여러 탭 열기/전환/닫기, 각 탭 독립 네비게이션·감지, Ctrl+Shift+T 복원
|
||||
- [ ] 탭에서 오디오 재생 시 스피커 아이콘 + 클릭 뮤트
|
||||
- [ ] 사이트별 줌 재방문 시 유지
|
||||
- [ ] 다운로드 매니저에서 페이지 다운로드 일시정지/재개/취소 + 속도 표시
|
||||
- [ ] 북마크바 표시/숨김(Ctrl+Shift+B) + 원클릭 이동
|
||||
- [ ] 종료 시 탭 저장 → 시작 시 복원(옵션)
|
||||
- [ ] `dotnet build` 0 오류/0 경고, 기존 미디어 감지·다운로드 회귀 없음
|
||||
|
||||
---
|
||||
|
||||
## 출처 (2차 조사 핵심 링크)
|
||||
|
||||
**WebView2 API (Microsoft Learn)**:
|
||||
- CoreWebView2.Find / CoreWebView2Find: learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2.find
|
||||
- 성능(서스펜션): learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/performance
|
||||
- 멀티 프로필: learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/multi-profile-support
|
||||
- 다운로드: …/corewebview2downloadoperation · 컨텍스트 메뉴: …/webview2/how-to/context-menus
|
||||
- WPF 플랫폼(airspace/CompositionControl): …/webview2/platforms/wpf
|
||||
|
||||
**Chrome/Edge**: support.google.com/chrome/answer/157179 (단축키) · …/2391819 (탭 관리) · explore.microsoft.com/en-us/edge/features (vertical tabs·sleeping tabs·split screen·web capture) · support.microsoft.com/…/keyboard-shortcuts-in-microsoft-edge
|
||||
|
||||
**Firefox/Vivaldi**: support.mozilla.org (keyboard shortcuts·reader view·ETP·HTTPS-only·PiP·screenshots) · help.vivaldi.com (tab-stacks·tab-tiling·workspaces·address-field·quick-commands·web-panels·page-actions)
|
||||
|
||||
**Zen/Arc**: docs.zen-browser.app (workspaces·split-view·glance·compact-mode·urlbar·shortcuts) · resources.arc.net (Spaces·Auto-Archive·Boosts·Peek·Air Traffic Control·Command Bar)
|
||||
|
||||
**Brave/Opera/기타**: brave.com/playlist · brave.com/shields · opera.com/features (tab-islands·video-popout·workspaces) · docs.floorp.app · Apple Support (Safari tab groups·reading list)
|
||||
|
||||
**전체 원문 리포트**: 5개 조사팀 상세 보고서는 세션 에이전트 결과로 보존됨.
|
||||
193
docs/DEPLOYMENT_PLAN.md
Normal file
193
docs/DEPLOYMENT_PLAN.md
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Video Downloader — 멀티플랫폼 배포 체계 계획서 (v1)
|
||||
|
||||
> **범위**: Windows · macOS · Linux 데스크톱 + Android · iOS 모바일의 **빌드→서명→패키징→배포→업데이트** 전 체계.
|
||||
> GitHub Releases 를 단일 배포 소스로, GitHub Actions 로 전 아티팩트 자동 빌드.
|
||||
>
|
||||
> **작성일**: 2026-08-15 · **관련 문서**: `BROWSER_FEATURES_PLAN.md`(v2) · `MOBILE_PLAN.md`(v1)
|
||||
> **조사 근거**: 전문 조사팀 20회 검색 + GitHub Releases API 로 툴체인 바이너리 실물 검증 (2026-08 기준)
|
||||
|
||||
---
|
||||
|
||||
## 0. 핵심 결론
|
||||
|
||||
1. **배포 소스는 GitHub Releases 단일 채널** — 계정/서버 인프라 없음(제품 철학과 일치). 태그(`v*`) 푸시 → CI 매트릭스 빌드 → Draft Release → QA → Publish.
|
||||
2. **⚠️ 중대 발견: CefGlue.Avalonia 는 Linux 미지원** (공식 문서 명시, 이슈 #189). MOBILE_PLAN/BACKLOG 의 "크로스플랫폼 브라우저 = CEF" 가정 수정 필요 → **플랫폼별 네이티브 WebView 계층**(Windows=WebView2, macOS=WKWebView, Linux=WebKitGTK)을 기본으로 하고 CEF는 Windows/macOS 보조 후보로 강등. 단, 네이티브 WebView 는 응답 가로채기(미디어 스니핑)가 CEF 보다 약함 → **크로스플랫폼 감지 품질 게이트를 별도로 통과해야 이관** (기존 Gate #1 정신 유지).
|
||||
3. **macOS 는 Apple Developer Program($99/년) 없이는 실효 배포 불가** (공증 필수). 첫 macOS 릴리스 전 가입 예산 수립.
|
||||
4. **데스크톱 자동 업데이트**: v1 = GitHub latest 태그 폴링 + 설치 열기(무의존·전 OS 공통), v2 = Velopack(델타 업데이트, Windows 우선 도입).
|
||||
5. **툴체인(yt-dlp/ffmpeg/deno)은 전 플랫폼 자동 다운로드 가능** — 단 ffmpeg macOS arm64 는 BtbN 빌드가 없어 **ffmpeg-static 릴리스** 사용 (§4 검증표).
|
||||
|
||||
---
|
||||
|
||||
## 0.5 자체 빌드머신 (운영 중 — 2026-08-15 구축 완료)
|
||||
|
||||
GitHub Actions 대신(또는 보완으로) **사용자 소유 빌드머신 2대**로 리눅스/macOS 산출물을 생산한다:
|
||||
|
||||
| 별칭 | 머신 | 사양 | SDK | 역할 |
|
||||
|---|---|---|---|---|
|
||||
| `vd-linux` | ChanpacaLaptop (Linux Mint 22.3) — Tailscale `100.65.55.115` | x86_64 · 32C · 62GB | 8.0.424 + 10.0.400 | `linux-x64` 빌드 |
|
||||
| `vd-mac` | Mac mini M2 (macOS 26.5.2) — LAN `192.168.0.127` / Tailscale `100.88.141.44` | arm64 | 8.0.424 + 10.0.400 | `osx-arm64` 빌드 (+iOS 임시) |
|
||||
|
||||
- **접속**: SSH 키 인증(`~/.ssh/id_ed25519_vd`, `~/.ssh/config` 별칭). 엔드포인트·자격증명은 `.env`(git 무시).
|
||||
- **빌드 명령**: `bash build/remote/build-remote.sh [linux|mac|all]` — git HEAD 소스를 머신에 전송 → Cli+Avalonia RID별 self-contained publish → 산출물 tar.gz 수집(`out/remote/<rid>/`).
|
||||
- **검증(2026-08-15)**: linux-x64·osx-arm64 양쪽 4산출물 생성(ELF/Mach-O arm64 매직 확인), **양 머신에서 네이티브 실행 E2E 통과** — HLS 테스트 스트림 64세그먼트 다운로드+병합 EXIT=0.
|
||||
- **운영 지식**: Avalonia 12.1 게터는 .NET 10 SDK 필요(8만 있으면 CS0103). 빌드머신 절전 시 SSH 끊김 → 재연결 감지 폴러 패턴으로 자동 재개 검증됨. Mac 무헤드 운용 시 "네트워크 접근 시 깨우기" 활성화 권장.
|
||||
- GitHub Actions(§5)는 리포지토리 공개 시 **자동 백업 채널**로 병행.
|
||||
|
||||
## 1. 제품 × 아티팩트 매트릭스
|
||||
|
||||
|
||||
| 제품 | 플랫폼 | 아티팩트 | 채널 | 서명 |
|
||||
|---|---|---|---|---|
|
||||
| **App (WPF+WebView2)** — 주력 | Windows x64 | `VideoDownloader-Setup-x64.exe` (Inno, 기존) | GitHub Releases | Authenticode (선택, 후순위) |
|
||||
| | Windows arm64 | `VideoDownloader-Setup-arm64.exe` (기존) | 〃 | 〃 |
|
||||
| **App (Avalonia 셸)** — 크로스플랫폼 이관 | Linux x64/arm64 | `videodownloader-linux-x64.tar.gz` (기본) + `.AppImage` + `.deb` | GitHub Releases | (GPG 서명 선택) |
|
||||
| | macOS x64/arm64 | `VideoDownloader-<arch>.dmg` (+ `.app` in DMG) | GitHub Releases | Developer ID + 공증 필수 |
|
||||
| **Mobile (MAUI)** | Android | `VideoDownloader-<ver>-arm64.apk` | GitHub Releases + Obtainium | Keystore (CI 시크릿) |
|
||||
| | Android (Play, 선택) | `.aab` | Google Play (추출코드 없는 빌드) | Play App Signing |
|
||||
| | iOS | `.ipa` | TestFlight → App Store | Apple 배포 인증서 |
|
||||
| **Server / 웹 컴패니언** | 데스크톱 앱에 내장 | 별도 배포 없음 (앱이 Kestrel 서빙) | — | 앱 서명에 포함 |
|
||||
|
||||
**버저닝**: `Major.Minor.Patch` 세마버 + 태그 `v*`. 모든 프로젝트 공통 버전(csproj `Version` 일괄 주입 `-p:Version=`).
|
||||
|
||||
---
|
||||
|
||||
## 2. 데스크톱 상세
|
||||
|
||||
### 2.1 Windows (현재 자산 유지)
|
||||
- 기존 체계 그대로: `dotnet publish -c Release -r win-x64/arm64 --self-contained`(폴더 배포) → ISCC 2회 → `out/Setup-*.exe`. `build/publish.ps1` 스크립트를 CI 잡으로 감싸기만 하면 됨.
|
||||
- Windows arm64 는 x64 러너에서 교차 publish 가능(현재 방식 그대로 CI 호환).
|
||||
|
||||
### 2.2 macOS (Avalonia 셸)
|
||||
1. `dotnet publish -c Release -r osx-arm64/-x64 --self-contained` (폴더; **CEF/단일파일 불가 원칙** — 네이티브 WebView 라면 폴더 배포로 충분)
|
||||
2. `.app` 래핑: `Contents/MacOS/`(호스트) · `Contents/Resources/` · `Info.plist` (Avalonia 공식 가이드)
|
||||
3. **universal 보다 아키텍처별 DMG 2개가 저렴** (lipo 병합은 호스트만 가능, CEF 프레임워크는 별도)
|
||||
4. 서명/공증 파이프라인:
|
||||
```
|
||||
codesign --force --options runtime --timestamp --sign "Developer ID Application: ..." App.app
|
||||
xcrun notarytool store-credentials NOTARY --apple-id ... --team-id ... --password ...
|
||||
xcrun notarytool submit App.dmg --keychain-profile NOTARY --wait
|
||||
xcrun stapler staple App.app
|
||||
```
|
||||
- DMG 생성: `create-dmg`/`dmgbuild` (macOS 러너에서만)
|
||||
- LAN 페어링(모바일 연동) 시 macOS 로컬 네트워크 프라이버시 프롬프트 대응 안내 UI 필요
|
||||
5. **툴체인 Gatekeeper 주의**: 앱이 HttpClient 로 내려받는 yt-dlp/deno/ffmpeg 는 quarantine xattr 이 안 붙음 → 런타임 자동다운로드 설계가 Gatekeeper 이슈를 자동 회피 (현재 설계 유지가 정답)
|
||||
|
||||
### 2.3 Linux (Avalonia 셸)
|
||||
- **기본 = tar.gz self-contained** (모든 배포판 보장), **편의 = AppImage** (linuxdeploy/appimagetool), **선택 = .deb** (dpkg-deb, `Depends:` 는 네이티브만).
|
||||
- AppImage 함정: Ubuntu 24.04 는 libfuse2 부재 → tar.gz 병행 의무화.
|
||||
- Avalonia(X11) 의존: `libx11-6 libice6 libsm6 libfontconfig1` (+브라우저 엔진이 WebKitGTK면 `libwebkit2gtk-4.1`, CEF면 Chrome 셋: `libnss3 libgbm1 libasound2 libatk1.0 libcups2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libdbus-1-3 libdrm2` + mesa).
|
||||
- 네이티브 데스크톱 항목(파일 다이얼로그 등)에 `libgtk-3` 선택 의존 가능.
|
||||
|
||||
### 2.4 크로스플랫폼 브라우저 엔진 전략 (수정)
|
||||
| OS | 1순위 | 대안 | 감지(스니핑) 전망 |
|
||||
|---|---|---|---|
|
||||
| Windows | **WebView2** (현행) | CefGlue | 최상 (WebResourceResponseReceived) |
|
||||
| macOS | **WKWebView** (WebView.Avalonia) | CefGlue(동작 보고됨) | WKURLSchemeHandler/프록시 조합 필요 — 품질 게이트 필수 |
|
||||
| Linux | **WebKitGTK** (WebView.Avalonia) | CEF(**미지원 이슈 #189**) | WebKit의 리소스 인터셉트로 제한적 — 품질 게이트 필수 |
|
||||
|
||||
→ **Gate #1'**: 3OS 각각 "HLS/DASH/MP4 직링크 감지 + 쿠키 전달" E2E 통과 시에만 해당 OS 베타 출시. 통과 못하면 해당 OS 는 "링크 붙여넣기 다운로더"(브라우저 없음, 현재 Avalonia 앱 형태)로 출시.
|
||||
|
||||
---
|
||||
|
||||
## 3. 모바일 상세
|
||||
|
||||
### 3.1 Android
|
||||
```bash
|
||||
dotnet publish src/Mobile -f net10.0-android -c Release \
|
||||
-p:AndroidPackageFormat=apk -p:RuntimeIdentifiers=android-arm64 \
|
||||
-p:AndroidKeyStore=true -p:AndroidSigningKeyStore=$KS \
|
||||
-p:AndroidSigningKeyAlias=vd -p:AndroidSigningKeyPass=... -p:AndroidSigningStorePass=...
|
||||
```
|
||||
- 배포: GitHub Releases APK + **Obtainium 지원** (README에 `app.json` 원탭 설정 배포 → GitHub Release 만으로 자동 업데이트 채널 확보)
|
||||
- Play (선택, 후순위): `AndroidPackageFormat=aab` + 사이트 특화 추출 코드 제거 빌드 변형
|
||||
|
||||
### 3.2 iOS
|
||||
- TestFlight 외부 10,000명 (베타 리뷰 24-48h). 내부 100명 무리뷰.
|
||||
- **필수**: `PrivacyInfo.xcprivacy` (2024-05부터 누락 시 ITMS-91053 리젝 — MAUI 바인딩 항목 포함 점검)
|
||||
- **심사 프레이밍 (가이드라인 2.5.13 부합)**: "사용자 소유 PC 원격 제어 + 자기 라이브러리 시청" — 심사 메모에 자기 PC 연결 데모 영상 첨부. 5.2.3 회피: 기기 내 추출 코드 없음(전부 PC 실행) 명시.
|
||||
- CI: macOS 러너에서 빌드+서명 (인증서/프로비저닝 시크릿), 업로드는 App Store Connect API.
|
||||
|
||||
---
|
||||
|
||||
## 4. 툴체인 바이너리 매핑 (GitHub Releases 검증 완료, 2026-08)
|
||||
|
||||
| 툴 | Windows x64/arm64 | macOS (universal/x64/arm64) | Linux x64/arm64 |
|
||||
|---|---|---|---|
|
||||
| yt-dlp | `yt-dlp.exe` / `yt-dlp_arm64.exe` | `yt-dlp_macos` (universal2) ✅ | `yt-dlp_linux` / `yt-dlp_linux_aarch64` ✅ |
|
||||
| ffmpeg | BtbN `win64-gpl` / `winarm64-gpl` | ⚠️ BtbN 없음 → **ffmpeg-static releases** (x64+arm64 darwin) | BtbN `linux64-gpl` / `linuxarm64-gpl` ✅ |
|
||||
| deno | `deno-x86_64-pc-windows-msvc.zip` | `deno-aarch64/x86_64-apple-darwin.zip` ✅ | `deno-x86_64/aarch64-unknown-linux-gnu.zip` ✅ |
|
||||
|
||||
- 공통 패턴: `https://github.com/<owner>/<repo>/releases/latest/download/<asset>` + SHA256 검증파시(.sha256sum/SUMS) — 기존 `YtDlpRunner`/`DenoRunner` 자동설치 메커니즘을 `Core/Platform/IToolLocator` 추상화 아래 **OS/아키텍처 매니페스트 테이블**로 일반화 (재구성 계획 ②와 동일 작업).
|
||||
- ffmpeg macOS 공급처 이원화(BtbN/ffmpeg-static)는 `IToolLocator` 구현체의 플랫폼별 다운로드 소스 매핑으로 흡수.
|
||||
|
||||
---
|
||||
|
||||
## 5. CI/CD (GitHub Actions)
|
||||
|
||||
```yaml
|
||||
# .github/workflows/release.yml (개요)
|
||||
on: { push: { tags: ['v*'] } }
|
||||
jobs:
|
||||
test: # dotnet test (단위+E2E) — 전 플랫폼 빌드의 관문
|
||||
desktop:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- { os: windows-latest, rid: win-x64 }
|
||||
- { os: windows-latest, rid: win-arm64 } # 교차 publish
|
||||
- { os: ubuntu-latest, rid: linux-x64 }
|
||||
- { os: ubuntu-24.04-arm, rid: linux-arm64 }
|
||||
- { os: macos-latest, rid: osx-arm64 } # 무료 M1 러너(공개 저장소)
|
||||
- { os: macos-13, rid: osx-x64 }
|
||||
steps: [checkout, setup-dotnet(8/10), publish(폴더/Inno/AppImage/deb/DMG), 서명·공증(macOS만), upload-artifact]
|
||||
android: [ubuntu-latest, JDK21+MAUI 워크로드, dotnet publish apk, 서명, upload]
|
||||
ios: [macos-latest, 워크로드, 서명(ipa), TestFlight 업로드]
|
||||
release:
|
||||
needs: [test, desktop, android]
|
||||
steps: [download-artifact, softprops/action-gh-release@v2 (draft: true)]
|
||||
```
|
||||
|
||||
- **시크릿**: `MACOS_P12`(base64)/`KEYCHAIN_PASS`/`NOTARY_PROFILE`(Apple ID·팀·앱비밀번호) · `ANDROID_KEYSTORE/ALIAS/PASS` · (iOS) `ASC_KEY`.
|
||||
- macOS 서명 함정: 키체인 자동 잠금 방지 타임아웃 설정.
|
||||
- 공개 저장소 기준 러너 무료(macOS 포함). 비공개 전환 시 macOS 10× 과금 유의.
|
||||
- QA 통과 후 Draft → Publish 수동 승격.
|
||||
|
||||
## 6. 업데이트 체계
|
||||
|
||||
| 대상 | 방식 |
|
||||
|---|---|
|
||||
| Windows/Linux/macOS v1 | `GET /repos/:owner/:repo/releases/latest` 태그 비교 → "다운로드·설치" 버튼(설치 파일 열기) |
|
||||
| Windows v2 | **Velopack** (델타+자동적용; Inno → Velopack 설치기 일회 마이그레이션 릴리스) |
|
||||
| Android | Obtainium (GitHub Releases 연동) / Play (후순위) |
|
||||
| iOS | TestFlight·App Store 표준 흐름 |
|
||||
| 툴체인 | 기존 yt-dlp 자동갱신 + `IToolLocator` 버전 체크 확장 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 로드맵 (배포 관점)
|
||||
|
||||
| 단계 | 내용 | 전제 |
|
||||
|---|---|---|
|
||||
| D1 | 리포지토리 GitHub 공개 + `release.yml`(test→win x64/arm64→draft) — **현재 Windows 체계를 CI 로 이식** | 없음 (즉시) |
|
||||
| D2 | Android APK 잡 추가 (M1 완료 시점) | M1 |
|
||||
| D3 | Linux tar.gz/AppImage/deb 잡 (Avalonia 셸 Gate #1' 통과 후) | 크로스플랫폼 셸 |
|
||||
| D4 | macOS DMG + 서명·공증 잡 (Developer Program 가입 후) | $99/년 + Gate #1' |
|
||||
| D5 | iOS TestFlight 잡 | M3 |
|
||||
| D6 | 자동 업데이트 v1(태그 폴링) → v2(Velopack) | D1 이후 언제든 |
|
||||
|
||||
## 8. 리스크 요약
|
||||
|
||||
1. **CEF Linux 미지원** — 네이티브 WebView 폴백 확정, 감지 품질 게이트로 방어 (§2.4)
|
||||
2. macOS 공증 = Apple Developer Program 필수 (미가입 시 macOS 배포 중단)
|
||||
3. AppImage/libfuse2, ffmpeg macOS 공급처 부재(BtbN) → ffmpeg-static 대체 (§4)
|
||||
4. iOS 프라이버시 매니페스트 누락 리젝 (ITMS-91053)
|
||||
5. 비공개 리포지토리 전환 시 macOS 러너 비용 10×
|
||||
|
||||
## 출처 (핵심)
|
||||
- CefGlue Linux 미지원: [OutSystems/CefGlue #189](https://github.com/OutSystems/CefGlue/issues/189) · [CefGlue.Avalonia NuGet](https://www.nuget.org/packages/CefGlue.Avalonia)
|
||||
- macOS 배포: [Avalonia macOS 가이드](https://avaloniaui.net/blog/the-definitive-guide-to-building-and-deploying-avalonia-applications-for-macos) · [.NET macOS publish](https://learn.microsoft.com/en-us/dotnet/core/deploying/macos) · [notarytool 워크플로](https://developer.apple.com/documentation/security/customizing-the-notarization-workflow)
|
||||
- Linux: [Avalonia Linux 의존성](https://docs.avaloniaui.net/docs/deployment/linux) · [deb-control](https://man7.org/linux/man-pages/man5/deb-control.5.html)
|
||||
- CI: [GitHub M1 러너](https://github.blog/changelog/2024-01-30-github-actions-introducing-the-new-m1-macos-runner-available-to-open-source/) · [애플 인증서 설치](https://docs.github.com/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development) · [action-gh-release](https://github.com/softprops/action-gh-release)
|
||||
- 툴체인: [yt-dlp releases](https://github.com/yt-dlp/yt-dlp/releases) · [BtbN ffmpeg](https://github.com/btbn/ffmpeg-builds/releases) · [ffmpeg-static](https://github.com/eugeneware/ffmpeg-static) · [deno releases](https://github.com/denoland/deno/releases)
|
||||
- 모바일: [MAUI publish-cli](https://learn.microsoft.com/en-us/dotnet/maui/android/deployment/publish-cli?view=net-maui-10.0) · [TestFlight 한도](https://developer.apple.com/help/app-store-connect/test-a-beta-version/invite-external-testers/) · [가이드라인 2.5.13](https://developer.apple.com/app-store/review/guidelines/) · [Obtainium](https://github.com/ImranR98/Obtainium)
|
||||
- 업데이트: [Velopack](https://github.com/velopack/velopack) · [cross-compile](https://docs.velopack.io/packaging/cross-compiling)
|
||||
162
docs/IMPLEMENTATION_AUDIT.md
Normal file
162
docs/IMPLEMENTATION_AUDIT.md
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
# 구현 감사 보고서 — 계획 ↔ 구현 ↔ 증거 매핑
|
||||
|
||||
> **대상**: `BROWSER_FEATURES_PLAN.md` (v2) + `MOBILE_PLAN.md` (v1) 전 항목
|
||||
> **감사일**: 2026-08-15 · **방법**: TDD(RED→GREEN) + E2E 검증
|
||||
> **요약**: 전 솔루션 Release 빌드 **0 오류**, 단위/E2E 테스트 **154/154 GREEN**, Android 에뮬레이터·iOS 시뮬레이터·LAN 서버 E2E 완료
|
||||
>
|
||||
> **감사 중 발견·수정된 회귀**: `HlsDownloader` 3-인자 ctor가 `_muxer` 미할당(재구성 ② 누락) → 모든 HLS 리먹스 NUL 위험. RED 테스트(`HlsMuxerWiringTests`)로 재현 후 ctor 할당 수정. `DashDownloader`는 정상.
|
||||
|
||||
---
|
||||
|
||||
## 1. 브라우저 기능 계획 (BROWSER_FEATURES_PLAN v2)
|
||||
|
||||
### Phase 0 — 퀵윈
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| T1-3 페이지 내 검색 (Ctrl+F) | ✅ 네이티브 `CoreWebView2.Find` + 자체 하단 바, 일치 수 X/Y | `Browser/FindSession.cs` · `WebView2FindE2ETests`(네이티브 Find 3매치) |
|
||||
| T1-4 검색엔진 | ✅ URL/검색어 판별, 기본 엔진, `g /b /d /n /yt ` 키워드 | `SearchEngineResolver.cs` + 단위테스트 |
|
||||
| T1-7 사이트별 줌 영속 | ✅ 도메인→배율 LRU(300), www 정규화, 클램프 0.1~5.0 | `SiteZoomStore.cs` + 단위테스트 |
|
||||
| T1-9 강제 새로고침 | ✅ Ctrl+Shift+R | `MainWindow.xaml.cs` 단축키 |
|
||||
| T1-11 전체화면 F11 | ✅ 크롬 자동 숨김 | `MainWindow.xaml.cs` |
|
||||
|
||||
### Phase A — 탭 아키텍처
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| 탭 시스템 (T1-1) | ✅ 공유 Environment + 탭당 WebView2, 생성/닫기/전환/Ctrl+Tab·1~9/드래그 정렬/Move | `Browser/Tabs/TabManager.cs` · `MainWindowSmokeE2ETests`(탭 생명주기 E2E) |
|
||||
| T1-2 닫은 탭 복원 | ✅ Ctrl+Shift+T, 스택 25개 | `TabManager.ReopenClosedTab` + 테스트 |
|
||||
| NewWindowRequested→새 탭 | ✅ deferral 패턴 | `TabViewService` NewTabProvider |
|
||||
| T1-8 오디오 표시/뮤트 | ✅ 스피커 아이콘 + 클릭 뮤트 | 탭 템플릿 `TabAudio_Click` |
|
||||
| T1-10 세션 복원 | ✅ session.json 저장/복원(옵션) | `TabSessionStore.cs` + 테스트 (VD_DATA_DIR 격리) |
|
||||
|
||||
### Phase B — 탭 보조
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| T1-5 다운로드 매니저 | ✅ 진행률/속도/일시정지/재개/취소/완료 정리 | `PageDownloadTracker.cs`(IDownloadOperation 주입) + 테스트 |
|
||||
| T1-6 북마크바 | ✅ Ctrl+Shift+B 토글, 원클릭 이동 | `MainWindow.xaml` bookmarkbar 행 |
|
||||
| T2-2 탭 절전 | ✅ 유휴 서스펜드 "zzz" | `TabSuspendService.cs` + 테스트 |
|
||||
| T2-8 탭 검색 | ✅ Ctrl+Shift+A, 열린+닫힘 탭 | `TabSearchFilter.cs` + 테스트 |
|
||||
| T2-12 우클릭 "이 링크 영상 받기" | ✅ 커스텀 컨텍스트 메뉴 | `TabViewService.ContextMenuBuild` |
|
||||
|
||||
### Phase C — 차별화
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| T2-1 명령 팔레트 | ✅ Ctrl+K 탭/북마크/기록/액션/웹검색 | `CommandPalette.cs` + 테스트 |
|
||||
| T2-3 분할 화면 | ✅ 2분할 + 스플리터 + Exit | `MainWindow` SplitCol/분할 단축키 |
|
||||
| T2-4 리더 모드 | ✅ Readability.js(Mozilla) 내장 → 스타일 본문, 원본 복원 | `ReaderMode.cs` · `ReaderModeE2ETests`(실제 추출+복원 E2E) |
|
||||
| T2-5 세로 탭 | ✅ 토글, 좌측 사이드 스트립 | `ToggleVerticalTabs` |
|
||||
| T2-6 Glance | ✅ Alt+클릭 플로팅 미리보기 | `GlanceWindow.cs` |
|
||||
| T2-7 사이트별 Boost | ✅ 강제 다크(invert-hue-rotate, 이미지/영상 보호) 등 | `BoostStore`/`BoostCssBuilder` + 테스트 |
|
||||
| T2-9 스크린샷 | ✅ Ctrl+Shift+S | `CapturePreviewAsync` |
|
||||
| T2-11 탭 그룹 | ✅ 6색 그룹, 인접 정렬, 그룹 닫기 | `TabManagerGroupExtensions` + 테스트 |
|
||||
|
||||
### Phase D — 시그니처
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| T2-10 미디어 재생목록 | ✅ 감지 미디어 자동 적재, 플라이아웃, 플레이어 창(native+hls.js+dash.js), 자동 다음, JSON 영속 | `MediaPlaylist.cs` · `PlayerWindow.cs` + 테스트 |
|
||||
| T3-1 워크스페이스 + 도메인 라우팅 | ✅ 저장/전환 + WorkspaceRoutingRules(도메인→워크스페이스, 주소창 이동 시 자동 전환, 팔레트로 규칙 등록) | `TabManagerWorkspaceExtensions` + `DesktopRemainderTests` 라우팅 2건(www/서브도메인 매칭·영속) |
|
||||
| T3-3 컨테이너/InPrivate | ✅ IsInPrivateModeEnabled 탭 | `OpenInPrivateTab` |
|
||||
| T3-4 빠른 찾기 `/` | ✅ | `MainWindow` 단축키 |
|
||||
| T3-5 컨텍스트 메뉴 전면 커스터마이징 | ✅ WPF 메뉴 + SelectedCommandId | `TabViewService` |
|
||||
| T3-7 PiP | ✅ 별도 최상위 창 | `TogglePip` |
|
||||
| T3-8 Speed Dial | ✅ 새 탭 다이얼 그리드 | `NewTabWithDial` |
|
||||
| T3-9 읽어주기(TTS) | ✅ | `SpeakText` |
|
||||
| T3-10 인쇄/소스보기 | ✅ Ctrl+P / Ctrl+U | `MainWindow` |
|
||||
|
||||
### Phase E — T3 잔여 완성 (2차)
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| T3-11 권한 UI | ✅ PermissionGate(출처×종류 기억, OtherSensors 자동거절/연속다운로드 자동허용) + PermissionRequested 배선 + 다이얼로그(항상 허용=SavesInProfile 영속) | `PermissionGateTests` 6건 + `PermissionTrackerE2ETests`(실제 WebView2 Notification.requestPermission → granted) |
|
||||
| T3-14 추적 방지 카운터 | ✅ TrackerBlocklist(20개 도메인, 서브도메인 매칭) + WebResourceRequested 차단(204) + TrackerCounter + 실드 배지·도메인 팝업 | `TrackerBlockTests` 8건 + E2E(실제 WebView2 에서 2개 스크립트 차단·집계, 정상 도메인 통과) |
|
||||
| T3-15 주소창 진행 바 + 파라미터 제거 복사 | ✅ NavProgressTracker(전 탭 집계 INPC) → 주소창 하단 인디케이터 + 주소창 우클릭 "파라미터 제거 후 복사"(UrlCleaner) | `UrlCleanerTests`/`NavProgressTrackerTests` 13건 |
|
||||
|
||||
### 3차 완성 (T3 잔여 + 스타일)
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| T3-2 탭 아카이브 | ✅ TabArchive(닫힘 이력 JSON 영속, 500 상한, 연속 중복 제목 갱신, 검색) + 탭 닫힘 배선 + 팔레트 "아카이브에서 열기" | `DesktopRemainderTests` 3건 + 스모크 E2E(URL 탭 닫음 → 아카이브 기록 실증) |
|
||||
| T3-6 페이지 액션 | ✅ BoostSettings.PageFilter(흑백/세피아) → CSS 필터 주입, 팔레트 명령 | `DesktopRemainderTests` 3건(RED→GREEN) + 스모크 E2E(로컬 HTTP 문서에서 적용→부스트 저장 실증) |
|
||||
| T3-12 노트 패널 | ✅ NotesStore(추가·검색·삭제·영속) + 노트 플라이아웃(Ctrl+Alt+N) + "현재 페이지 추가" | `DesktopRemainderTests` 2건 + 스모크 E2E + 실창 스크린샷 |
|
||||
| T3-13 컴팩트 모드 | ✅ Ctrl+Shift+M 크롬 자동 숨김 + 상단 엣지(4px) 호버 복원 | 스모크 E2E(행 높이 0→복원) + 실창 스크린샷 |
|
||||
| 스타일 폴리시 | ✅ 폰트 폴백(한국어 맑은 고딕 포함) + 암시 TextBlock 폰트 통일 + 다크 ToolTip + 텍스트 계층 대비 상향(Secondary/Muted/Border) + 상태바 가독성 + 다이얼 페이지 타이포 | 실창 스크린샷 검증(다이얼/노트/컴팩트) |
|
||||
|
||||
### 재구성 (사용자 지시 "근원부터 뜯어고쳐")
|
||||
|
||||
| 작업 | 결과 |
|
||||
|---|---|
|
||||
| Detection → `VideoDownloader.Core.Detection` 이동 | App 의존성 역전 |
|
||||
| `Core/Platform/` 신설: `IProcessRunner`/`ProcessSpec`/`DesktopProcessRunner`(stdout·stderr 동시 콜백 계약 문서화), `IMediaMuxer`/`DesktopMediaMuxer`(-progress 파싱) | 모바일/크로스플랫폼 재사용 준비 (MOBILE_PLAN §1.4 추상화 4곳 중 3곳) |
|
||||
| `VideoDownloader.Browser` 라이브러리 (탭/검색/줌/세션) | god-class 해체 |
|
||||
| `TabViewService` 추출 (탭별 WebView2 배선 전담) | MainWindow 코드비하인드 축소 |
|
||||
|
||||
## 2. 모바일 계획 (MOBILE_PLAN v1)
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| **M0** Core 추상화 | ✅ IProcessRunner/IMediaMuxer/경로(CorePaths, VD_DATA_DIR 오버라이드) | `PlatformAbstractionTests`(cmd.exe 통합, FakeProcessRunner) |
|
||||
| **M0** `VideoDownloader.Server` | ✅ Kestrel, Bearer+쿼리 토큰(상수시간), /api/pair·queue CRUD·library·stream(206 Range)·file·웹 컴패니언 | `RemoteServerE2ETests`(401/pair/queue/library/Range 206/file/웹컴패니언 전 경로) |
|
||||
| **M0** QR 페어링 다이얼로그 | ✅ QRCoder, LAN IP 자동 | `PairingWindow.cs` |
|
||||
| **M0** 검증 관문: 웹 컴패니언 | ✅ 제로 설치 페이지(#t= 프래그먼트 페어링, 2초 폴링, video 스트리밍) | `WebCompanion.cs` + E2E |
|
||||
| **M1** QR 스캔 페어링 + 수동 폴백 | ✅ ZXing(실기기) + 수동 IP:포트+토큰 | 에뮬레이터 E2E에서 수동 페어링으로 실증 (IME 제약) |
|
||||
| **M1** 큐 목록 실시간 + 제어 | ✅ 2초 폴링(계획 허용안: "v1은 2초 폴링 가능"), 일시정지/재개/취소 버튼 | 에뮬 E2E: 라이브 서버 데이터 렌더 실증 |
|
||||
| **M1** 링크 붙여넣기 추가 | ✅ URL 엔트리 + "PC에서 받기" | 페이지 UI + 서버 API E2E (POST 400 검증 포함) |
|
||||
| **M1** 공유 타겟(U1) | ✅ ACTION_SEND 인텐트 필터("PC에서 받기") + MainActivity OnCreate/OnNewIntent → ShareTarget 채널 → 루트 2s 타이머가 SharePayload.ExtractUrl → POST /api/queue | `SharePayload` 단위 7건(RED→GREEN) · 에뮬 E2E: **`am start -a SEND --es EXTRA_TEXT "…https://youtu.be/E2ESHARE123"` → 서버 큐에 해당 URL 반영 확인** + dumpsys 로 인텐트 필터 등록 확인 |
|
||||
| **M2** 라이브러리 목록 | ✅ 파일명/크기 | 에뮬 E2E: sample.mp4/415,253 bytes 렌더 |
|
||||
| **M2** MediaElement 스트리밍 재생 | ✅ Range HTTP → 모달 플레이어 | 에뮬 E2E: **실제 MP4 프레임 렌더 실증**(testsrc 패턴 스크린샷) + 서버 ESTABLISHED |
|
||||
| **M2-U3** 이어보기 위치 동기화 | ✅ `POST/GET /api/position/{id}` + PositionStore(positions.json 영속) + 플레이어 5초 주기·종료 시 저장, 재생 시작 시 시크 | RED→GREEN `이어보기_위치_동기화_E2E` · 에뮬 E2E: positions.json 13.3→24.2→**35.2초**(재오픈 시 저장 위치에서 이어 재생 실증) |
|
||||
| **M2-U4** 폰 오프라인 저장 + 저장 탭 | ✅ ApiClient.DownloadFileAsync(.part→rename, 진행률) + OfflineStore(manifest.json) + 라이브러리 💾 버튼 + 저장 탭(오프라인 재생/삭제) | 에뮬 E2E: ✓ 저장됨 → 저장 탭 "561,312 bytes — 오프라인" → **서버 정지 후 재생 실증**(프레임 픽셀 비교: 4초 간 5,935px 변경 + 컬러 패턴 렌더) → 🗑 삭제 동작 |
|
||||
| **M3** net10.0-ios | ✅ macOS 전용 TFM 조건 분기 | vd-mac 빌드: Mach-O arm64 .app, Info.plist 검증 |
|
||||
| **M3** iOS 시동 | ✅ | iPhone 17 시뮬레이터(iOS 26) 설치+실행, 전체 UI 렌더 스크린샷 |
|
||||
|
||||
### 모바일 E2E에서 발견·수정된 결함 (TDD 외 가치)
|
||||
|
||||
1. 탭 자식 `PushAsync` 불가(NavigationPage 부재) → 루트 모달 `PushModalAsync` + `NavigationPage` 래핑
|
||||
2. `CollectionView.SelectionChanged` Android 비발화 → 템플릿 카드 `TapGestureRecognizer`
|
||||
3. 링커가 `Xamarin.AndroidX.LocalBroadcastManager` 제거 → Release 크래시 → 명시 참조
|
||||
4. iOS `NSCameraUsageDescription` 누락 → 시작 크래시 → Info.plist 추가 (iOS+MacCatalyst)
|
||||
5. 시뮬레이터 카메라 세션이 첫 프레임 블록 → 스캐너를 실기기(Physical)로 게이트
|
||||
|
||||
### 2차 완성 (M2 잔여 + U5)
|
||||
|
||||
| 항목 | 구현 | 증거 |
|
||||
|---|---|---|
|
||||
| U5 완료 알림 | ✅ `VideoDownloader.Mobile.Core`(net8 순수 로직) QueueCompletionDetector(첫 폴링 무시·재알림 방지) + Plugin.LocalNotification(Android 13+ 권한) | `MobileCoreTests` 9건 + 에뮬 E2E: 데모 큐 완료 전환 → **"다운로드 완료" 노티 실제 게시**(dumpsys notification) |
|
||||
| 재생중 미디어 알림 | ✅ 플레이어 열릴 때 ongoing 노티 "재생 중"(채널 vd_playback), 닫히면 취소 | 에뮬 E2E: 게시·소멸 lifecycle 실증(dumpsys). **FGS 미사용** — 재생은 앱 내(포그라운드) 한정이므로 ongoing 노티로 충족, 문서화 |
|
||||
| 라이브러리 그리드(썸네일·해상도) | ✅ 서버 Thumbnailer(ffmpeg 1초 프레임 320px JPEG + 해상도 ffprobe, 사이드카 .thumb.jpg/.meta.json 캐시) + `GET /api/library/{id}/thumb` + DTO Resolution + 앱 2열 그리드 | `ThumbnailerTests` 4건(Fake 러너) + 서버 E2E(JPEG 매직/404/401) + 에뮬 E2E: **실제 ffmpeg 썸네일 렌더**(스크린샷)·해상도 "480x270" |
|
||||
| 외부망 안내(§7) | ✅ ConnectionTroubleshoot(같은 네트워크/Tailscale 안내) → QueuePage 표시 | 단위 3건 + 에뮬 E2E: 죽은 포트 페어링 → "같은 네트워크… Tailscale 등 VPN" 문구 실증 |
|
||||
| 오프라인 저장 진행 알림(M2) | ✅ 💾 저장 중 Android ProgressBar 노티(5% 스로틀 갱신) → 완료 시 "폰 저장 완료" 노티로 전환 | 에뮬 E2E: 34MB 저장 중 "폰에 저장 중 — N%" 라이브 관측(t=1~12s) → 완료 후 **"폰 저장 완료"(big.mp4) 레코드 실증**(진행→완료 전환, Report(1) 되덮어쓰기 방지 포함) |
|
||||
| U4 저장소 로직 이관 | ✅ OfflineStore → Mobile.Core(디렉터리 주입, MAUI 비의존) | 재시작 매니페스트 유지 테스트 포함 |
|
||||
|
||||
### 명시적 미구현 (계획 허용/후순위)
|
||||
|
||||
- SignalR 허브 — 계획안 명시 "v1은 2초 폴링으로도 가능" 채택
|
||||
- mDNS 광고(_videodl._tcp) — QR 페어링이 주 경로(계획 §6 리스크 대응과 동일 전제)
|
||||
- 미디어 알림의 FGS 화 — 백그라운드 재생이 없는 컴패니언 구조상 ongoing 노티로 대체(위 표)
|
||||
- M4 Android 독립 다운로더 — 계획상 "선택"
|
||||
- iOS TestFlight — 앱 실행/페어링 프레이밍까지만 (감사일 기준)
|
||||
|
||||
## 3. 배포 (DEPLOYMENT_PLAN)
|
||||
|
||||
| 항목 | 상태 |
|
||||
|---|---|
|
||||
| Windows 로컬 빌드 | ✅ Release 0 오류 |
|
||||
| linux-x64 셸(Cli+Avalonia) self-contained | ✅ vd-linux 빌드+ELF 검증 → `out/remote/linux-x64/` |
|
||||
| osx-arm64 셸 | ✅ vd-mac 빌드+Mach-O 검증 → `out/remote/osx-arm64/` |
|
||||
| Android APK | ✅ x86_64 Release 서명 APK(에뮬 설치/실행 실증) |
|
||||
| iOS .app | ✅ vd-mac iossimulator-arm64 빌드 |
|
||||
| 빌드머신 SSH 키 프로비저닝 | ✅ `build/remote/install-buildkey.ps1` |
|
||||
|
||||
## 4. 성공 기준 체크 (계획 §7)
|
||||
|
||||
- [x] `dotnet build` 전체 0 오류 — 8개 프로젝트(App/Avalonia/Browser/Cli/Core/Mobile/Server/Tests)
|
||||
- [x] 기존 미디어 감지·다운로드 회귀 없음 — Detection 이동 후 단위/E2E GREEN
|
||||
- [x] 폰 브라우저 웹 컴패니언 큐 조회/추가 (M0) — 서버 E2E
|
||||
- [x] Android 앱 큐 실시간 갱신/제어 (M1) — 에뮬 E2E 라이브 데이터
|
||||
- [x] 라이브러리 스트리밍 재생(시크 포함) (M2) — 실측 MP4 프레임 + 206 Range
|
||||
- [x] iOS 빌드 시동 + 큐 조회 (M3) — iOS 26 시뮬레이터 UI 렌더
|
||||
- [x] 폰 저장 오프라인 재생 (M2 후반) — 서버 정지 상태 프레임 렌더 실증 + 이어보기 동기화(재개 35.2초)
|
||||
246
docs/MOBILE_PLAN.md
Normal file
246
docs/MOBILE_PLAN.md
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
# Video Downloader — 모바일 연동 + 모바일 앱 계획서 (v1)
|
||||
|
||||
> **목표**: Windows 비디오 다운로더(내장 브라우저)에 **모바일 앱을 만들고 PC↔모바일을 연동**한다. 핵심 질문 — "모바일에서 이 앱과 무엇을 하게 할 것인가?"에 대한 조사 기반 답변.
|
||||
>
|
||||
> **작성일**: 2026-08-15 · **관련 문서**: `BROWSER_FEATURES_PLAN.md` (v2, 데스크톱 브라우저 기능) · `DEPLOYMENT_PLAN.md` (Win/macOS/Linux/모바일 배포 체계)
|
||||
>
|
||||
> **조사 방법론**: 4개 전문 조사팀 병렬 심층 조사 — ① 모바일 브라우저 UX(Chrome/Safari/Firefox/Samsung/Brave 모바일) ② PC↔모바일 연동 패턴(20여 개 제품: Chrome Send Tab, Handoff, Phone Link, LocalSend, KDE Connect, Jellyfin 등) ③ 모바일 영상 생태계(iOS/Android 제약, 다운로더 앱 생존 모델, 미디어 서버) ④ 기술 스택 검증(**실제 코드베이스 grep 포함**). 총 73회 웹 검색 + 공식 문서 열람.
|
||||
|
||||
---
|
||||
|
||||
## 0. 핵심 결론 (Executive Summary)
|
||||
|
||||
1. **제품 형태 = "컴패니언 앱" 우선**. PC가 일꾼(추출·다운로드), 폰은 리모컨+뷰어. 이는 Jellyfin/VLC 리모컨/Chromecast 모델로 검증된 구조이며, 양쪽 앱스토어 정책 리스크도 최소화함.
|
||||
- 폰에서: ① 링크 보내 PC가 받게 하기 ② 다운로드 큐 원격 제어(진행률 실시간) ③ PC 라이브러리 스트리밍 시청 ④ 완료된 영상 폰으로 저장(오프라인)
|
||||
2. **아키텍처 = 클라우드 없는 LAN 직결**. 데스크톱 앱에 Kestrel 서버를 내장(REST + SignalR), QR 코드로 페어링(주소+토큰 동시 전달). LocalSend/KDE Connect가 증명한 계정 없는 지속 가능한 모델. 원격 접근은 자체 릴레이 대신 Tailscale/VPN 문서화.
|
||||
3. **기술 스택 = .NET MAUI 10 (net10.0-android/ios)**. 기존 `VideoDownloader.Core`(net8.0) 엔진을 **~90% 그대로 재사용** — 모바일 비호환 지점은 단 4곳(§4 검증 표). Flutter/네이티브는 Core 재사용이라는 이 프로젝트의 존재 이유를 포기해야 하므로 기각. Avalonia는 데스크톱 실험용 유지.
|
||||
4. **플랫폼 전략 = Android 먼저(APK/GitHub 배포), iOS는 컴패니언 프레이밍**.
|
||||
- **Android**: 자유도 최대 — 나중에 독립 다운로더 모드 추가 가능(YoutubeExplode 순수 C#).
|
||||
- **iOS**: WebKit 강제 사실상 지속 + **Apple이 Brave의 오프라인 영상 저장을 금지한 전례** → 기기 내 추출 코드 배제, "내 PC 라이브러리 원격 시청" 프레이밍만.
|
||||
5. **제로 설치 웹 컴패니언 겸비**: 데스크톱 앱이 폰 브라우저용 페이지를 직접 서빙 → 앱 설치 없이 QR 찍으면 즉시 큐 모니터링 가능(1차 검증 수단).
|
||||
|
||||
---
|
||||
|
||||
## 1. 조사 결과 종합
|
||||
|
||||
### 1.1 모바일 브라우저 UX 관례 (앱 UI의 기준선)
|
||||
|
||||
| 관례 | 내용 | 출처 |
|
||||
|---|---|---|
|
||||
| **하단 주소창 기본** | Chrome 2025 정식 탑재(롱프레스로 전환), Safari/iOS 26도 상하단 선택. 단 Chrome의 실수(탭 버튼 숨김)는 반면교사 — 하단 바에 탭 버튼 유지 | Chrome/Safari |
|
||||
| **전체 화면 탭 그리드** | 카드 썸네일 + 스와이프 닫기 + 탭 검색, 탭 수 배지 | 전 브라우저 |
|
||||
| **제스처** | 화면 엣지 스와이프=뒤로/앞으로, 주소창 가로 스와이프=탭 전환, 당겨서 새로고침(비활성화 가능), 툴바 아래로 스와이프=몰입 모드 | Chrome/Safari/Samsung |
|
||||
| **다운로드 관리자** | ⋮ 메뉴 속 풀페이지 화면(데스크톱 셸프와 다름). **우리 앱은 다운로드가 본업이므로 하단 바 전용 슬롯 + 감지 배지로 승격** | Chrome Android |
|
||||
| **영상 감지 UI** | **Samsung Video Assistant**(영상 위 오버레이 → 팝업 플레이어), **Brave Playlist**(탭 아이콘 → 재생목록 추가) — 데스크톱 감지 배너의 모바일 등가물 | Samsung/Brave |
|
||||
| **리더/다크** | URL 옆 아이콘(Safari 배치), 시스템 테마 추종 + 강제 다크 토글(Firefox Night Mode) | Safari/Firefox |
|
||||
|
||||
### 1.2 PC↔모바일 연동 — 제품별 매트릭스 (20개 제품 조사)
|
||||
|
||||
| 제품 | 전송물 | 페어링 | 구조 |
|
||||
|---|---|---|---|
|
||||
| Chrome Send Tab | 탭/링크 | **계정**(클라우드) | 클라우드 동기화+푸시 |
|
||||
| Safari Handoff | 앱 상태 통째로 | iCloud+BLE 근접 | P2P Wi-Fi(AWDL) |
|
||||
| Phone Link | 알림/SMS/사진 | BT+MS 계정 | 하이브리드 |
|
||||
| Firefox Send Tab | 탭 | **QR 페어링**(FxA) | **E2E 암호화 릴레이** |
|
||||
| **LocalSend** | 파일/텍스트 | 같은 Wi-Fi+PIN | **순수 LAN, 계정 없음** |
|
||||
| **KDE Connect** | 파일/클립보드/알림/입력 | LAN+TLS 인증서 확인 | **순수 LAN** |
|
||||
| **Jellyfin** | 라이브러리/제어 | 서버 URL+API 키 | **자체 호스팅 REST+WebSocket** |
|
||||
| VLC 리모컨 | 재생 제어 | 없음(HTTP 인터페이스) | 순수 LAN |
|
||||
| Chromecast | 미디어 URL 위임 | mDNS | **폰=리모컨만, 미디어는 직접** |
|
||||
| Intel Unison | 파일/SMS/알림 | 계정 | **2025-06 서비스 종료** ⚠️ |
|
||||
|
||||
**도출된 원칙**:
|
||||
- 계정 기반 클라우드 동기화(Chrome/Edge)는 인프라+유지 비용 — 인디 규모에서 Unison처럼 죽는다. **계정 없는 LAN 모델이 지속 가능한 니치**.
|
||||
- **QR 페어링 = 발견+키 교환을 동시에** 해결하는 최고의 UX 트릭(WhatsApp Web/Firefox). `http://<ip>:<port>/#t=<토큰>`.
|
||||
- LocalSend 보안 모델 차용: 자가서명 인증서 지문 양쪽 화면 표시(목측 검증), 선택적 PIN, 전송 전 수락 프롬프트("폰이 다운로드 3건 요청 — 수락?").
|
||||
- **크로미캐스트 교훈**: 폰은 URL/제어 패킷만 보내고 미디어 바이트를 프록시하지 않는다. 폰 앱은 큐 대상 **stateless** 설계(앱을 꺼도 PC 다운로드는 계속).
|
||||
- 미러링하지 말 것: 알림 미러링(특권+유지비), BLE 근접(데스크톱 전파 신뢰성 낮음), 상시 클립보드 동기화(프라이버시 표면).
|
||||
|
||||
### 1.3 모바일 영상 생태계 — 플랫폼 제약표
|
||||
|
||||
| 항목 | iOS | Android |
|
||||
|---|---|---|
|
||||
| 다운로더 정책 | 5.2.2 위반 시 리젝. **Brave Playlist 오프라인 저장 금지 전례**. 생존 모델 = "파일 관리자+브라우저"(Documents by Readdle) 프레이밍 | Play도 실질 금지(TubeMate 퇴출). **탈출구 = APK/F-Droid/GitHub 배포**(NewPipe·Seal·YTDLnis 전부) |
|
||||
| 백그라운드 다운로드 | `NSURLSession` 백그라운드(앱 종료해도 계속, 강제종료 시 예외) | FGS `dataSync`(~6h 제한·폐지 예정) → **WorkManager가 정답**(YTDLnis 방식) |
|
||||
| 백그라운드 재생/PiP | Background Modes + AVAudioSession `.playback` | `mediaPlayback` FGS + 상시 알림 |
|
||||
| 파일 저장 | 샌드박스만. 사진 보관함/Files 내보내기 | MediaStore.Downloads(자기 파일은 권한 불필요) |
|
||||
| 스트리밍 프로토콜 | **AVPlayer = HLS만, DASH 불가** | ExoPlayer/Media3 = HLS+DASH 네이티브 |
|
||||
| yt-dlp 실행 | **불가능**(서브프로세스 금지) | 가능(Termux제 Python 번들) — 단 GPL, Play 배포 불가 |
|
||||
|
||||
- **미디어 서버 참고**: Jellyfin(REST+WebSocket, 다운로드/포지션 동기화), **Streamyfin**(클라이언트 측 HLS→로컬 파일 변환으로 "다운로드" 구현 — 재생 가능=다운로드 가능 구조), Plex(코덱 호환 시 원본 직접, 아니면 트랜스코드).
|
||||
- **오프라인 라이브러리 UX 기대치**(Netflix/Spotify): 전용 다운로드 탭, 화질/크기 선택기+저장공간 대시보드, 백그라운드 큐, 재시작 생존.
|
||||
- **수요 배경**: YouTube가 2026년 비프리미엄 백그라운드 재생 루트를 봉쇄 중 → "내 라이브러리 무제한 백그라운드/PiP"는 진짜 차별점.
|
||||
|
||||
### 1.4 기술 스택 검증 (실제 코드 grep 기반)
|
||||
|
||||
**스택 비교 평결**:
|
||||
|
||||
| 스택 | Core 재사용 | 모바일 생태계 | 평결 |
|
||||
|---|---|---|---|
|
||||
| **.NET MAUI 10** | ~100% (컴패니언+독립 모두) | SignalR 공식 지원, ZXing.Net.Maui, MediaElement | ✅ **채택** |
|
||||
| Avalonia 12 모바일 | 동일 | 얇음(QR 스캐너/플레이어 없음, 네이티브 직접 붙여야) | 데스크톱 실험 유지 |
|
||||
| Flutter | 0% (Dart 재작성) | 최고 | 기각 — Core 재사용 포기 |
|
||||
| Kotlin/Swift 네이티브 | 0% | 최고 | 기각 — 코드 2벌 |
|
||||
|
||||
**버전 주의**: MAUI 8/9는 이미 지원 종료 → 신규 MAUI 앱은 **.NET 10 대상 필수**. 다행히 `net10.0-android`는 기존 `net8.0` Core 어셈블리를 그대로 참조 가능.
|
||||
|
||||
**`VideoDownloader.Core` 모바일 공유 분석 (파일:라인 검증 완료)**:
|
||||
|
||||
| 상태 | 위치 | 내용 |
|
||||
|---|---|---|
|
||||
| ✅ 그대로 사용 | `Hls/HlsDownloader`(병합 호출 제외), `M3u8Parser`, `Dash/DashParser·Models`, `Models/*`, `Social/CookieRecord·FormatOption`, `Util/FileNameUtil` | HttpClient 기반 — 모바일 완전 호환 |
|
||||
| ⚠️ 추상화 필요 | `Social/YtDlpRunner.cs:131-181`, `DenoRunner.cs` | `Process.Start` → `IProcessRunner`/`IToolLocator` 인터페이스로 추출 |
|
||||
| ⚠️ 추상화 필요 | `Hls/HlsDownloader.cs:237`, `Dash/DashDownloader.cs:71` | ffmpeg 서브프로세스 → `IMediaMuxer` |
|
||||
| ⚠️ 추상화 필요 | `Config/AppConfig.cs` | `SpecialFolder.MyVideos` → 플랫폼 경로 주입 |
|
||||
|
||||
→ **추상화 4곳만 하면 Core ~90% 공유.** 이 리팩터는 BACKLOG의 Avalonia 이관 로드맵 2단계(공유 리팩터)와 **동일한 작업** — 한 번의 리팩터로 모바일+크로스플랫폼 모두 준비됨 (시너지).
|
||||
|
||||
**Android 독립 모드용 YouTube**: Python 번들 대신 **YoutubeExplode**(순수 C#, Tyrrrz의 YoutubeDownloader 15.8k★가 사용) — MAUI 안에서 동작.
|
||||
|
||||
---
|
||||
|
||||
## 2. 유즈케이스 정의 (컴패니언 앱)
|
||||
|
||||
| # | 유즈케이스 | 흐름 | 근원 패턴 |
|
||||
|---|---|---|---|
|
||||
| U1 | **링크 → PC로 받기** | 폰 공유 시트(YouTube/웹페이지) → "PC에서 받기" → PC 대기열 즉시 추가 + 수락 프롬프트 | Chrome Send Tab + LocalSend 수락 프롬프트 |
|
||||
| U2 | **큐 원격 제어** | 큐 목록 실시간(진행률/속도), 일시정지/재개/취소/재시도 | Jellyfin 리모컨 |
|
||||
| U3 | **라이브러리 스트리밍** | PC 저장 폴더 그리드(썸네일+메타) → 탭하여 시청(Range HTTP 스트리밍, 이어보기 위치 양방향 동기화) | Jellyfin/Streamyfin |
|
||||
| U4 | **폰으로 저장(오프라인)** | 라이브러리 항목 다운로드 → 앱 오프라인 라이브러리(Neflix식 다운로드 탭) | Plex Downloads |
|
||||
| U5 | **완료 알림** | PC에서 다운로드 완료 → 폰 푸시(연결 중일 때) | — |
|
||||
| U6 | **웹 컴패니언(제로 설치)** | 폰 카메라로 QR → 폰 브라우저에서 바로 큐/라이브러리 열람 | PairDrop/Jellyfin 웹 |
|
||||
| U7 | (후기) **Android 독립 다운로더** | PC 없이 폰에서 직접 HLS/DASH/YouTube 받기 | YTDLnis/YoutubeExplode |
|
||||
|
||||
---
|
||||
|
||||
## 3. 아키텍처 설계
|
||||
|
||||
```
|
||||
┌────────────────── Windows 앱 (WPF) ──────────────────┐
|
||||
│ WebView2 브라우저 + 감지 DownloadManager (Core) │
|
||||
│ ▲ │
|
||||
│ ┌───────────────┴────────────────┐ │
|
||||
│ VideoDownloader.Server (신규 클래스 라이브러리) │
|
||||
│ · Kestrel (동적 포트, ListenAnyIP) │
|
||||
│ · Bearer 페어링 토큰 (256bit, QR로 배포) │
|
||||
│ · mDNS 광고 (_videodl._tcp, Makaretu.Dns) │
|
||||
│ · 정적 웹 컴패니언 페이지 서빙 (제로 설치 클라이언트) │
|
||||
└──────┬───────────────┬──────────────┬────────────────┘
|
||||
│ LAN HTTP(S) │ SignalR │ Range HTTP
|
||||
┌──────▼─────┐ ┌──────▼─────┐ ┌─────▼──────┐
|
||||
│ MAUI 앱 │ │ 폰 브라우저 │ │ 미디어 │
|
||||
│ (Android→ │ │ (웹 컴패니언)│ │ 재생 │
|
||||
│ iOS) │ └────────────┘ └────────────┘
|
||||
└────────────┘
|
||||
```
|
||||
|
||||
**API 설계안** (LocalSend 프로토콜 + Jellyfin 참고):
|
||||
- `POST /api/pair` — 토큰 검증 + 기기명 등록
|
||||
- `POST /api/queue` — URL 추가(U1; 데스크톱 수락 프롬프트 연동)
|
||||
- `GET /api/queue` · `POST /api/queue/{id}/pause|resume|cancel|retry` — 큐 제어(U2)
|
||||
- `GET /api/library` (+`/{id}/thumb`) — 메타데이터 카탈로그(U3; ffprobe/FFMpegCore 썸네일, 사이드카 캐시)
|
||||
- `GET /stream/{id}` — MP4 Range 요청(206) 스트리밍(U3; `Results.File(enableRangeProcessing:true)`)
|
||||
- `GET /file/{id}` — 폰 저장용 다운로드(U4)
|
||||
- `/hubs/queue` — SignalR 진행률/로그 푸시(U2/U5; v1은 2초 폴링으로도 가능)
|
||||
- `POST /api/position/{id}` — 이어보기 위치 동기화(U3)
|
||||
|
||||
**보안 모델**: 페어링 토큰(Bearer, 상수시간 비교) + 재페어링 시 재생성. LAN HTTP는 iOS ATS `NSAllowsLocalNetworking` 예외로 허용. 원격(외부망)은 자체 릴레이 대신 **Tailscale 문서화**(Syncthing 커뮤니티 표준 패턴).
|
||||
|
||||
**데스크톱 UI 연결점**: 설정에 "모바일 연동" 섹션 → QR 다이얼로그(QRCoder) + 페어링된 기기 목록 + 원격 액세스 토글. 브라우저 계획(v2)의 명령 팔레트에 "기기로 보내기" 액션 추가 가능.
|
||||
|
||||
---
|
||||
|
||||
## 4. 모바일 앱 기능 목록 (Tier)
|
||||
|
||||
### Tier M1 — 최소 컴패니언 (Android APK)
|
||||
- [ ] QR 스캔 페어링(ZXing.Net.Maui) + 수동 IP:포트 폴백
|
||||
- [ ] 큐 목록 + 실시간 진행률(SignalR or 폴링) + 일시정지/재개/취소/재시도
|
||||
- [ ] 공유 타겟: 다른 앱에서 URL 공유 → "PC에서 받기" (U1)
|
||||
- [ ] 링크 붙여넣기 직접 추가
|
||||
- [ ] 웹 컴패니언 페이지(데스크톱이 서빙) — 앱 없이 검증용
|
||||
|
||||
### Tier M2 — 라이브러리 + 시청
|
||||
- [ ] 라이브러리 그리드(썸네일·제목·해상도·크기·추가일)
|
||||
- [ ] MediaElement 스트리밍 재생(Range HTTP) — 백그라운드 오디오
|
||||
- [ ] 이어보기 위치 동기화
|
||||
- [ ] 폰으로 다운로드(오프라인 저장) + 다운로드 탭(Neflix식) + Android 진행 알림
|
||||
- [ ] Android 재생중 미디어 알림(FFS mediaPlayback)
|
||||
|
||||
### Tier M3 — iOS 확장
|
||||
- [ ] net10.0-ios 타겟 + Info.plist 키 3종(ATS/로컬네트워크/Bonjour)
|
||||
- [ ] TestFlight 배포, "내 PC 라이브러리 리모컨" 프레이밍 (기기 내 추출 코드 엄격 배제)
|
||||
|
||||
### Tier M4 — Android 독립 다운로더 (선택, PC 없는 모드)
|
||||
- [ ] Core HLS/DASH/직접 다운로더 + 모바일 muxer 어댑터
|
||||
- [ ] YoutubeExplode로 YouTube 직접 수신
|
||||
- [ ] WorkManager 백그라운드 큐 + 재시작 생존
|
||||
- [ ] 내장 브라우저 탭(하단 주소창·탭 그리드·제스처 관례 준수, §1.1) + 감지 배지
|
||||
- [ ] 저장: MediaStore.Downloads
|
||||
|
||||
### 시기상조/미차용 (명시적 제외)
|
||||
- 클라우드 동기화/계정, 알림 미러링, 클립보드 동기, BLE 근접 전송, 자체 릴레이 서버, iOS 기기 내 yt-dlp(기술적·정책적 불가)
|
||||
|
||||
---
|
||||
|
||||
## 5. 로드맵 (의존성 순)
|
||||
|
||||
### Phase M0 — 데스크톱 준비 (필수 선행, 1~2주)
|
||||
- [ ] **Core 추상화 리팩터**: `IProcessRunner`·`IToolLocator`·`IMediaMuxer`·경로 제공자 인터페이스 추출 (데스크톱 구현은 무변화, 기존 4개 프로젝트 무영향. BACKLOG Avalonia 로드맵 2단계와 동일 작업)
|
||||
- [ ] **`VideoDownloader.Server` 신규 라이브러리**: Kestrel 내장 + 토큰 인증 + `/api/pair`·`/api/queue` + SignalR 허브
|
||||
- [ ] WPF 설정에 QR 페어링 다이얼로그(QRCoder) + mDNS 광고(Makaretu.Dns)
|
||||
- [ ] **검증 관문**: 폰 브라우저로 QR 스캔 → 웹 컴패니언에서 큐 조회/추가 동작 (앱 개발 전 서버 완성 입증)
|
||||
- ⚠️ git 아님 → `git init` 후 착수 (브라우저 계획 Phase A와 동일 전제)
|
||||
|
||||
### Phase M1 — Android 컴패니언 앱 (2~3주)
|
||||
- MAUI 10(net10.0-android) 신규 프로젝트 `VideoDownloader.Mobile` + Core 참조
|
||||
- Tier M1 전체. **APK/GitHub Releases 배포** (Play 정책 회피 — YTDLnis 선례)
|
||||
|
||||
### Phase M2 — 라이브러리/시청 (2~3주)
|
||||
- 데스크톱: ffprobe 메타데이터 + 썸네일 캐시, `/api/library`·`/stream/{id}`(Range)
|
||||
- 앱: Tier M2 전체
|
||||
|
||||
### Phase M3 — iOS (1~2주 + 심사)
|
||||
- Tier M3. TestFlight 우선.
|
||||
|
||||
### Phase M4 — Android 독립 모드 (선택, M2 이후 언제든)
|
||||
- Tier M4. 브라우저 계획(BROWSER_FEATURES_PLAN v2)의 탭 아키텍처 경험을 모바일 WebView UI에 재활용.
|
||||
|
||||
**브라우저 계획과의 순서 권고**: 데스크톱 브라우저 기능(탭 등)과 모바일은 독립 트랙. 다만 **Core 추상화(M0-1)만 먼저** 해두면 양쪽이 자유로워짐. 서버(M0-2)는 브라우저 작업과 무관하게 병행 가능.
|
||||
|
||||
---
|
||||
|
||||
## 6. 리스크 & 대응
|
||||
|
||||
| 리스크 | 대응 |
|
||||
|---|---|
|
||||
| MAUI 품질(iOS 성능/바인딩 이슈 보고) | 컴패니언 규모(리스트·플레이어·스캐너)는 수용 가능한 범위. Android 먼저 출시해 리스크 분산 |
|
||||
| iOS 5.2.2 리젝 (다운로더 오인) | 기기 내 추출 코드 배제 + "내 PC 리모컨" 프레이밍 + Brave 전례 문서화. TestFlight로 시작 |
|
||||
| Windows 방화벽 첫 프롬프트 | 최초 비로컬 바인딩 시점에 안내 UI("허용" 가이드) |
|
||||
| 멀티캐스트 차단 네트워크(기업/캠퍼스) | QR 페어링이 주 경로(발견 불필요) + 로컬 IP 프로브 폴백(LocalSend 방식) |
|
||||
| iOS 로컬 네트워크 권한 거부 시 무음 실패 | 권한 거부 감지 → 설정 안내 화면 |
|
||||
| Play 정책(독립 모드) | APK/GitHub 배포로 회피. Play 출시 시 사이트 특화 추출 코드 제거 빌드 |
|
||||
| AVPlayer DASH 불가 | 컴패니언은 MP4/HLS 스트리밍만(앱 원본은 대부분 MP4). DASH 원본은 데스크톱에서 필요 시 ffmpeg 실시간 트랜스코드(후순위, Jellyfin 계층 구조 참고) |
|
||||
| yt-dlp 인자 인젝션(YTDLnis RCE 전례, SonarSource) | U1 공유 URL 화이트리스트 검증 + 인자 배열 방식(이미 데스크톱 `OpenInExplorer`에서 동일 교훈 적용 완료) |
|
||||
|
||||
---
|
||||
|
||||
## 7. 성공 기준 (완료 정의)
|
||||
|
||||
- [ ] PC 앱 켠 상태에서 폰으로 QR 스캔 → 설치 없는 웹 컴패니언에서 큐 조회/URL 추가 성공 (M0)
|
||||
- [ ] Android 앱: 공유 시트로 YouTube 링크 → PC 대기열 자동 추가 + PC 수락 → 다운로드 시작 (M1)
|
||||
- [ ] Android 앱: 큐 진행률 실시간 갱신 + 일시정지/재개/취소 동작 (M1)
|
||||
- [ ] 폰에서 PC 라이브러리 그리드 탐색 → 스트리밍 재생(탐색/시크 포함) → 앱 종료 후 재개 시 이어보기 (M2)
|
||||
- [ ] 라이브러리 항목 폰 저장 → 오프라인 재생 (M2)
|
||||
- [ ] iOS TestFlight 빌드 시동 + 페어링 + 큐 조회 (M3)
|
||||
- [ ] `dotnet build` 전체 0 오류 — 기존 4개 프로젝트(WPF/Avalonia/Cli/Core) 회귀 없음 (M0~전체)
|
||||
- [ ] 외부망에서 연결 시도 → 명확한 "같은 네트워크 필요/Tailscale 안내" 메시지 (UX)
|
||||
|
||||
---
|
||||
|
||||
## 출처 (핵심)
|
||||
|
||||
- **연동 패턴**: [LocalSend 프로토콜 v2.2](https://github.com/localsend/protocol) · [KDE Connect](https://github.com/kde/kdeconnect-kde) · [Firefox FxA 페어링 아키텍처](https://mozilla.github.io/ecosystem-platform/explanation/pairing-flow-architecture) · [Mozilla Sync E2E](https://hacks.mozilla.org/2018/11/firefox-sync-privacy/) · [Syncthing P2P 분석](https://advancedweb.hu/how-syncthing-provides-secure-file-syncing-without-sharing-your-files-with-a-third-party/) · [Intel Unison 종료](https://www.pcworld.com/article/2836520/intels-excellent-unison-pc-to-phone-app-shuts-down-for-good.html)
|
||||
- **모바일 브라우저 UX**: [Chrome 하단 주소창](https://blog.google/products-and-platforms/products/chrome/address-bar-position-change/) · [Samsung Video Assistant](https://r2.community.samsung.com/t5/Tech-Talk/Samsung-Internet-web-browser-Video-Assistant/td-p/5923119) · [Brave Playlist](https://brave.com/playlist/) · [iOS 오프라인 금지 공지](https://community.brave.app/t/notice-playlist-offline-video-on-ios-disallowed-by-apple/654965)
|
||||
- **플랫폼 제약**: [Apple 5.2.2 가이드라인](https://developer.apple.com/app-store/review/guidelines/) · [Android 14 FGS 타입](https://developer.android.com/about/versions/14/changes/fgs-types-required) · [iOS 백그라운드 다운로드](https://developer.apple.com/documentation/foundation/downloading-files-in-the-background) · [TN3179 로컬 네트워크 프라이버시](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy) · [AVPlayer HLS-only](https://mux.com/articles/hls-vs-dash-what-s-the-difference-between-the-video-streaming-protocols)
|
||||
- **생태계**: [youtubedl-android](https://github.com/yausername/youtubedl-android) · [YTDLnis](https://github.com/deniscerri/ytdlnis) · [YTDLnis 인자 인젝션 RCE](https://www.sonarsource.com/blog/ytdlnis-argument-injection-rce/) · [Streamyfin](https://github.com/streamyfin/streamyfin) · [Jellyfin 트랜스코딩](https://jellyfin.org/docs/general/post-install/transcoding/) · [Documents by Readdle](https://support.readdle.com/documents/built-in-browser-download-manager/documents-internal-web-browser)
|
||||
- **기술 스택**: [MAUI 지원 정책](https://dotnet.microsoft.com/en-us/platform/support/policy/maui) · [Rick Strahl: 데스크톱 내장 ASP.NET 서버](https://weblog.west-wind.com/posts/2023/Nov/27/Embedding-a-minimal-ASPNET-Web-Server-into-a-Desktop-Application) · [SignalR 지원 플랫폼](https://learn.microsoft.com/en-us/aspnet/core/signalr/supported-platforms) · [ZXing.Net.Maui](https://github.com/redth/ZXing.Net.Maui) · [MAUI MediaElement](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/maui/views/mediaelement) · [Makaretu.Dns.Multicast](https://richardschneider.github.io/net-mdns/articles/sd.html) · [QRCoder](https://github.com/codebude/QRCoder) · [YoutubeExplode](https://github.com/tyrrrz/youtubeexplode) · [Kestrel Range 처리](https://github.com/dotnet/aspnetcore/issues/25230)
|
||||
Loading…
Add table
Add a link
Reference in a new issue