QR 스캐너 게이트 수정: DeviceType 휴리스틱 오판(실기기에서 스캐너 미표시) — iOS 시뮬레이터만 폴백, Android 는 항상 스캐너

This commit is contained in:
Yun Chan 2026-08-16 18:32:41 +09:00
parent 0cb16c8830
commit bbbd83fe58
6 changed files with 5 additions and 1 deletions

219
docs/BACKLOG.md Normal file
View 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` 권장.