releases/docs/BACKLOG.md
Yun Chan 02995f4153 MEDIABROWSER 개명·업데이트 채널 전환·아이콘 tofu 근본 수정 (BACKLOG 13차) — ①개명: installer.iss AppName/출력 MediaBrowser-Setup-*(AppId·exe·데이터 경로 유지로 업그레이드 호환), 워크플로 산출물명(mediabrowser-*.tar.gz, MediaBrowser-<ver>-arm64.apk), 창 타이틀·모바일 표시명·obtainium.json·README. ②UpdateChecker: apiBase 주입(기본=자체 Forgejo /api/v1, GitHub 는 apiBase 로 호환), osx 자산 tar.gz 허용(기존 dmg 만 → mac 영원히 자산 없음 결함 수정), AppConfig 기본 UpdateRepo=Video-Downloader/releases+UpdateApiBase — 테스트 4건. ③아이콘 tofu(사용자 보고 "절대 안고쳐지네") 근본 원인: Shared.xaml 암시적 TextBlock 스타일의 FontFamily=UiFont 강제가 버튼 ContentPresenter 자동 TextBlock 의 IconFont 상속을 덮어 모든 PUA 글리프가 notdef — 폰트 설치/글리프 존재/HW·SW 렌더/RTB/DPI/런타임 8·10/자체포함·공유 전부 배제 후 실창 픽셀 비교로 격리, 콘텐츠 TextBlock 의 실폰트가 Pretendard 체인임을 테스트로 실증. 수정: 암시 스타일 FontFamily 제거(창 루트 상속으로 통일, PairingWindow/PlayerWindow 는 SetResourceReference), PUA+텍스트 혼합 콘텐츠 7곳+코드 1곳 정리, 순수 글리프 2곳 IconFont 명시. 회귀 방어: IconGlyphXamlTests(정적 스캔)·IconContentFontTests(실창 폰트 단정)·IconGlyphRenderTests(픽셀 비교) — 462/462 GREEN, 수정 전후 실창 캡처로 글리프 렌더 확인
2026-08-18 07:13:18 +09:00

311 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 아님 → `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-<ver>-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차와 동일 경로(변경 없음). 산출물 개명은 다음 리허설/태그에서 반영 확인 예정.