# 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 ` 로 위임. 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 ` 가 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** — `x64;arm64`, `win-x64;win-arm64`, `SelfContained=true`(폴더, single-file/trim 아님 — WPF+트리밍 위험 회피). `PlatformTarget` 하드코딩 제거(RID에서 파생). - [x] **`build/publish.ps1`** — x64/arm64 루프: `dotnet publish -c Release -r -p:Platform= --self-contained` 후 ISCC(`/DARCH=`)로 `out/VideoDownloader-Setup-.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 아님 → `git init` 권장~~ → git 전환 완료. ## 9차 팔로업 (2026-08-16 완료) — 계획 문서 잔여 갭 소거 (SignalR·mDNS·자동 업데이트·CI) > 목표 "만들어진 모든 계획 문서 달성"에 따라 MOBILE_PLAN·DEPLOYMENT_PLAN 의 미이행 항목 일괄 구현. - [x] **SignalR 큐 허브 `/hubs/queue`** — MOBILE_PLAN §3 아키텍처 항목. `QueueHub`(GetQueue 스냅샷) + `IQueueAdapter.Changed` → `queueUpdated` 전 클라이언트 푸시. 인증 미들웨어에 `/hubs` 편입(`t`/`access_token` 쿼리 허용) + `UseWebSockets`. 실 Kestrel E2E 로 접속·푸시·401 검증. (`Server/QueueHub.cs`, `ServerHost.cs`) - [x] **mDNS 광고 `_videodl._tcp`** — MOBILE_PLAN M0 항목. `MdnsAdvertiser`(Makaretu.Dns) + `ServerOptions.EnableMdns`(App 은 켬). 실패는 `ServerHost.MdnsError` 로 표면화(발견 보조 — 서버 기동 불가로 번지지 않게). Makaretu 가 TXT 에 ASCII 만 허용 → 한글 기기명은 퍼센트 인코딩(무손실), 인스턴스 라벨은 ASCII 접기. **광고→발견 실증**: 별도 브라우저 인스턴스가 `_videodl._tcp.local` 수신(RESULT=DISCOVERED). (`Server/MdnsAdvertiser.cs`) - [x] **자동 업데이트 v1** — DEPLOYMENT_PLAN §6/D6. `Core/Update/UpdateChecker`: GitHub Releases latest 태그 비교(관대한 v접두/4자리 파싱), RID별 설치 자산 선택(arm64 우선 배제 규칙), GitHub UA 규약. 설정창 "앱 업데이트" 섹션(`AppConfig.UpdateRepo`) — 새 버전이면 릴리스 페이지 열기(설치는 사람 관문). 가짜 핸들러 격리 테스트 + 실 창 UI 테스트. - [x] **CI 릴리스 파이프라인 스캐폴드** — DEPLOYMENT_PLAN §5/D1~D3. `.github/workflows/release.yml`: 태그 `v*` → test 관문(win) → Windows 설치자(x64/arm64, Inno) · Linux/macOS tar.gz(Cli+Avalonia) · Android APK(키스토어 시크릿 조건부 서명) → Draft Release. iOS 잡은 `ENABLE_IOS` 변수 게이트(Apple 계정 전제). `build/obtainium.json` + README 배포 절 추가. - **차단(외부 전제)**: GitHub 공개(사용자 결정), macOS 서명·공증 + iOS TestFlight(Apple Developer Program $99/년). ### 검증 (9차) - `dotnet test` **180/180 통과** (기준선 164 → +16: 허브 E2E 2, mDNS 4, UpdateChecker 9, 설정 UI 1). - 빌드 오류 0 / 신규 경고 0 (기존 CS8602 1건 유지). - mDNS 실증: ServerHost(EnableMdns) 기동 → 별도 ServiceDiscovery 로 15초 내 발견 성공. ## 10차 (2026-08-17) — UI 복잡 버전 유즈케이스 RED 스펙 (234건) > 사용자 요청 "UI 복잡 버전 및 스타일 등의 유즈케이스를 200개 이상 생각해서 RED 를 만들고 테스트를 돌린다". > TDD RED 단계 산출물 — 구현(GREEN) 전. 스펙 전문: `docs/UI_USECASES.md`. - [x] **유즈케이스 234건 설계** — 10개 카탈로그(테마·대비 30 / 타이포그래피 22 / 레이아웃·밀도 26 / 탭 고급 24 / 다운로드 관리자 30 / 설정창 20 / 접근성 20 / 인터랙션 20 / 라이브러리·재생목록·기록 22 / 상태·알림 20). 각 유즈케이스 = 테스트 1건과 1:1 대응. - [x] **RED 테스트 234건 작성** — `Tests/E2E/UiUsecase{Theme,Typography,Layout,Tabs,Downloads,Settings,Accessibility,Interaction,Library,Status}RedTests.cs`. 검증 방식: 소스 XAML/코드비하인드 직접 읅기(신규 `Infrastructure/SourceTree.cs` — 루트 탐색·브러시 대비 계산·리플렉션 계약). - [x] **실측 결함 발견(대비)** — 다크 TextMuted/CardBg 4.42:1, CardBgHover 4.01:1, 라이트 TextMuted/CardBg 4.22:1·GlassBg 3.53:1, 다크 AccentBrush/CardBg 4.38:1 (모두 WCAG AA 4.5:1 미달), 라이트 Track/Panel 1.18:1·다크 Border/Card 1.38:1 (비텍스트 3:1 미달). - [x] **GREEN 구현 완료(2026-08-17)** — 234/234 통과. 토큰·테마·설정 계약·대비 수정(라이브 테마 전환 포함)·다운로드 관리자·라이브러리·접근성·인터랙션 전 구현. 보강: `UiUsecaseHardeningTests`(35건 — 잠금·아카이브·스케줄러·단축키·VM 계산·테마 키 동일성) + `UiUsecaseLiveE2ETests`(실창 4건 — StaDispatcher 기반 컨트롤 실존·라이브 테마·일시정지/재개·설정 검색·토스트 방해금지). 총 453/453 GREEN. ### 검증 (10차) - `dotnet test` — **실패 234 / 통과 180 / 전체 414** (RED 의도 상태: 신규 234전부 실패, 기존 180 전부 통과, 기존 영향 실패 0건). - 빌드 오류 0 / 신규 경고 0. ## 11차 (2026-08-17) — Forgejo 자체 러너 릴리스 CI 가동 (전 플랫폼 빌드→배포 폐쇄루프) > 사용자: "러너들이 생겼다 이제 우리도 빌드를 모두 다 배포할 수 있다" — 저장소를 자체 Forgejo > (`git.chanpaca.net/Video-Downloader/client`)로 이관한 데 이어, org 러너 3대로 릴리스 CI 구축. §5-bis 참조. - [x] **`.forgejo/workflows/release.yml`** — 태그 `v*` → test 관문(전체 xUnit) → windows(설치자 x64/arm64)·android(APK)·linux(tar.gz×2 RID)·macos(tar.gz×2 RID) 병렬 → 각 잡이 draft 릴리스에 자산 직접 업로드. 러너 3대(yunchan-win/kd-desktop/yunchan-mac) 모두 host 모드라 `uses:` 액션 전무 — 체크아웃도 `git fetch`+`FETCH_HEAD` 직접. workflow_dispatch = `ci-rehearsal` draft prerelease 리허설. - [x] **`build/ci/upload-release-asset.{ps1,sh}`** — draft 릴리스 확보(병렬 잡 동시 생성 경합 시 재조회) + 동명 자산 교체 멱등. 실 API 검증 중 Windows Git Bash 에서 python 출력 CRLF 로 자산 id 가 `5\r` 로 오염돼 DELETE 가 조용히 실패하는 결함 발견·수정(`tr -d '\r'`). - [x] **android MSB3030 수정** — CLI 전역 `-p:RuntimeIdentifiers=android-arm64` 가 참조 프로젝트(net8.0 Mobile.Core)까지 전파돼 RID 경로 dll 부재로 실패. 9차 GitHub 스캐폴드에 잠재하던 결함이 첫 실전 실행에서 실증 → Mobile csproj 의 `CiPackArm64Only` 게이트 조건부 `RuntimeIdentifiers` 로 이전. - [x] **인프라** — `RELEASE_TOKEN` 시크릿(PAT `write:repository`) API 등록, ALPACA-HOME 유령 Inno Setup(레지스트리만 잔존) 재설치, `.sh` 실행 비트. Forgejo 는 `.forgejo/workflows` 만 인식 — `.github/workflows`(GitHub 공개 시 백업 채널)와 이중 실행 없음 확인. ### 검증 (11차) - 리허설 1차(b05e8bf): test **453/453 통과**(실창 E2E 포함 — 러너가 사용자 세션이라 창 생성 가능) · windows/linux/macos 성공 · android 실패(MSB3030). - 리허설 2차(d98c734, 수정 후): **5/5 잡 전부 성공** — `ci-rehearsal` draft prerelease 자산 7개: Setup x64 57.2MB·arm64 51.5MB / linux tar.gz x64 71.1MB·arm64 68.1MB / osx tar.gz arm64 69.6MB·x64 72.7MB / APK(arm64-v8a 단독) 23.3MB. - 정식 배포: `git tag v1.0.0 && git push origin v1.0.0` → 동일 파이프라인이 `v1.0.0` draft 릴리스 생성 → QA 후 수동 Publish (§7.5 런북 그대로). ## 12차 (2026-08-18) — MEDIABROWSER 배포 사이트 + 공개 릴리스 저장소 > 사용자: "릴리즈 페이지 만들자 — 프로모션(패럴랙스·글래스모피즘·라이트) + 릴리스 + 사용 안내, > wrangler 로 Cloudflare 배포, 파일은 NAS 호스팅, 배포명은 MEDIABROWSER". DEPLOYMENT_PLAN §5-ter 참조. - [x] **`site/` 3페이지** — index(프로모션: 히어로 스플릿+벤토+기능 그리드+모바일+플랫폼 밴드, CSS scroll-driven 패럴랙스 + IntersectionObserver 등장, `prefers-reduced-motion/transparency` 존중, JS 실패 시에도 내용이 보이는 `html.js` 게이트) · releases(플랫폼 그룹·OS 자동 감지 권장·이전 버전 접힘·빈 상태) · guide(설치→첫 다운로드→관리→휴대폰 연결→FAQ). Wanted Sans Variable + Phosphor 스프라이트. - [x] **실앱 자산** — 히어로/가이드의 다운로드 패널 이미지는 `VD_DATA_DIR` 격리 프로필로 띄운 실제 앱 창 캡처(밝기 보정). 가짜 목업 없음. - [x] **공개 릴리스 저장소** — 소스(client)가 비공개라 자산 익명 다운로드 불가 → `Video-Downloader/releases` (공개) 신설, CI 업로드를 env `RELEASE_REPO` 로 전환. draft 는 익명에 안 보여 QA 후 Publish 흐름 유지. 생성 직후 저장소 설명이 인코딩 깨짐(EFBFBD)으로 올라간 문제(사용자 보고)는 UTF-8 파일 기반 PATCH 로 교정. - [x] **배포** — `build/deploy-site.ps1`(releases.json 생성 → wrangler pages deploy). 라이브: https://mediabrowser-7r4.pages.dev (mediabrowser 서브도메인 선점 → 접미). ### 검증 (12차) - Playwright 로 3페이지 전체 스크린샷 검증(1440px): 히어로/벤토/기능/모바일/밴드 렌더, 릴리스 빈 상태, 샘플 데이터 주입 렌더(플랫폼 4그룹·권장 강조·프리뷰 필), 가이드 목차·FAQ. 라이브 URL 에서 재확인. - 도중 결함 2건 수정: fullPage 캡처로 드러난 reveal 무한 숨김 위험(no-JS 백지) → `html.js` 게이트, 패널 캡처의 사용자 세션 노출 위험 → 격리 프로필 재캡처로 교체. ## 13차 (2026-08-18) — 후속 과제 일괄: 개명·업데이트 채널·사이트 자동화 + 아이콘 tofu 근본 수정 > 사용자: "ㄱㄱ 다해" — 12차에서 남긴 후속 5건 진행. 아이콘은 "절대 안고쳐지네" 보고를 받아 근본 원인까지 추적. - [x] **MEDIABROWSER 개명(배포명)** — installer.iss(AppName/출력 MediaBrowser-Setup-*, AppId GUID·exe·데이터 경로는 유지해 업그레이드 호환), publish.ps1, 양쪽 릴리스 워크플로 산출물명(mediabrowser-*.tar.gz, MediaBrowser--arm64.apk), 창 타이틀·환영 문구·모바일 ApplicationTitle, obtainium.json(공개 저장소 URL), README·release 스킬. - [x] **UpdateChecker 채널 전환** — `CheckAsync(..., apiBase)` 주입(기본 `https://git.chanpaca.net/api/v1`, GitHub 는 apiBase 지정으로 호환), osx 자산에 tar.gz 허용(기존 dmg 만 허용 → mac 영원히 자산 없음 결함), AppConfig 기본 UpdateRepo=`Video-Downloader/releases` + UpdateApiBase. 테스트 4건 추가. - [x] **릴리스 Publish 시 사이트 자동 재배포** — releases 저장소에 `site-deploy.yml`(release published → CLIENT_TOKEN 으로 client site/ 체크아웃 → releases.json 생성 → wrangler 배포). 시크릿 3종 API 등록, workflow_dispatch 검증 성공(deploy success). - [◐] **커스텀 도메인** — Pages 프로젝트에 mediabrowser.chanpaca.net 등록 완료. DNS CNAME 은 CLOUDFLARE_API_TOKEN/CF_API_TOKEN 모두 Zone DNS 편집 권한이 없어 보류 — **사용자 액션**: 대시보드에서 `mediabrowser` CNAME → `mediabrowser-7r4.pages.dev`(프록시 ON) 추가하면 즉시 활성. - [x] **아이콘 tofu 근본 수정** — 원인: Shared.xaml 의 암시적 TextBlock 스타일이 FontFamily=UiFont 를 강제 → 버튼 ContentPresenter 자동 TextBlock 이 IconFont 상속을 잃고 모든 PUA 글리프가 notdef(□). 폰트 설치·글리프 존재· 렌더 경로(HW/SW·RTB·DPI·런타임 8/10·자체포함/공유) 전부 배제 후 실창 픽셀 비교로 격리. 수정: 암시 스타일의 FontFamily 강제 제거(창 루트 상속으로 통일), PairingWindow/PlayerWindow 에 SetResourceReference. 부수 결함: PUA+텍스트 혼합 콘텐츠 7곳+코드 1곳(항상 tofu 로 렌더되던 것) 정리, 순수 글리프 2곳 IconFont 명시. - 미해결 관찰(후속): 첫 실행(빈 프로필)에서 초기 탭이 HomeUrl 로 자동 탐색되지 않고 빈 화면으로 남는 현상. ### 검증 (13차) - `dotnet test` — **462/462 통과** (기준선 453 → UpdateChecker 4·IconGlyphXaml 2·IconGlyphRender 2·IconContentFont 1 추가). - 아이콘: 수정 전 실창 캡처(□ tofu) → 수정 후 동일 조건 캡처에서 ‹ › ↻ ⌂ 글리프 정상 렌더 확인. 실창 회귀 테스트가 콘텐츠 TextBlock 의 FontFamily=Segoe Fluent Icons + 본문 Pretendard 유지를 단정. - android 로컬 재현·리허설은 12차와 동일 경로(변경 없음). 산출물 개명은 다음 리허설/태그에서 반영 확인 예정.