19 KiB
19 KiB
구현 감사 보고서 — 계획 ↔ 구현 ↔ 증거 매핑
대상:
BROWSER_FEATURES_PLAN.md(v2) +MOBILE_PLAN.md(v1) 전 항목 감사일: 2026-08-15 (갱신 2026-08-16) · 방법: TDD(RED→GREEN) + E2E 검증 요약: 전 솔루션 Release 빌드 0 오류, 단위/E2E 테스트 180/180 GREEN, Android 에뮬레이터·iOS 시뮬레이터·LAN 서버 E2E 완료감사 중 발견·수정된 회귀:
HlsDownloader3-인자 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 렌더 스크린샷 |
| M3 iOS 네트워킹(페어링+큐 조회, §7) | ✅ Info.plist ATS(NSAllowsLocalNetworking)+NSLocalNetworkUsageDescription | vd-mac 로컬 데모 서버 + 시뮬 NSUserDefaults 페어링 시딩 → "iOS E2E 큐 항목 — 77%" 진행바 라이브 렌더 실증(http LAN 통신 = ATS 예외 작동) |
모바일 E2E에서 발견·수정된 결함 (TDD 외 가치)
- 탭 자식
PushAsync불가(NavigationPage 부재) → 루트 모달PushModalAsync+NavigationPage래핑 CollectionView.SelectionChangedAndroid 비발화 → 템플릿 카드TapGestureRecognizer- 링커가
Xamarin.AndroidX.LocalBroadcastManager제거 → Release 크래시 → 명시 참조 - iOS
NSCameraUsageDescription누락 → 시작 크래시 → Info.plist 추가 (iOS+MacCatalyst) - 시뮬레이터 카메라 세션이 첫 프레임 블록 → 스캐너를 실기기(Physical)로 게이트
- DeviceType 휴리스틱이 실기기에서 가상으로 오판해 스캐너 미표시(사용자 제보) → 게이트를 "iOS 시뮬레이터만"으로 좁혀 Android 는 항상 스캐너
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 비의존) | 재시작 매니페스트 유지 테스트 포함 |
3차 완성 (2026-08-16) — 계획 잔여 갭 소거 라운드
| 항목 | 구현 | 증거 |
|---|---|---|
SignalR 큐 허브 (MOBILE_PLAN §3 /hubs/queue) |
✅ QueueHub(스냅샷 GetQueue) + IQueueAdapter.Changed → 전 클라이언트 queueUpdated 푸시. 인증은 기존 미들웨어에 /hubs 편입(쿼리 t/access_token 허용). 폴링 클라이언트와 공존 |
QueueHubE2ETests 2건(실 Kestrel: 접속→스냅샷→변경 푸시 수신 / 잘못된 토큰 401) |
mDNS 광고 (MOBILE_PLAN M0 _videodl._tcp) |
✅ MdnsAdvertiser(Makaretu.Dns) — ServerOptions.EnableMdns, 실패는 ServerHost.MdnsError 로 표면화(발견 보조라 서버는 유지). 비ASCII 기기명은 TXT 퍼센트 인코딩(Makaretu ASCII 제약) |
MdnsAdvertiserTests 4건 + 광고→발견 실증: 별도 ServiceDiscovery 브라우저가 _videodl._tcp.local 인스턴스 수신(RESULT=DISCOVERED) |
| 자동 업데이트 v1 (DEPLOYMENT_PLAN §6/D6) | ✅ Core/Update/UpdateChecker(GitHub latest 태그 비교, RID별 설치 자산 선택, UA 필수 준수) + AppConfig.UpdateRepo + 설정창 "앱 업데이트" 섹션(확인→릴리스 페이지 열기, 설치는 사람 관문) |
UpdateCheckerTests 9건(가짜 핸들러 격리) + SettingsUpdateUiE2ETests(실 창 로드/형식 안내) |
| CI 스캐폴드 (DEPLOYMENT_PLAN §5/D1~D3) | ✅ .github/workflows/release.yml(test 관문→win x64/arm64 Inno·linux/macos tar.gz·android apk→Draft Release) + build/obtainium.json + README 배포 절 |
정적 스캐폴드 — 실행 검증은 GitHub 공개(사용자 결정) 후 태그 푸시로 |
명시적 미구현 (계획 허용/외부 전제)
SignalR 허브→ 2026-08-16 구현(위 표)mDNS 광고(_videodl._tcp)→ 2026-08-16 구현(위 표)- 미디어 알림의 FGS 화 — 백그라운드 재생이 없는 컴패니언 구조상 ongoing 노티로 대체(위 표). 계획 문서에 대체 결정 명기됨
- M4 Android 독립 다운로더 — 계획상 "선택" (성공 기준 §7 에 미포함)
- iOS TestFlight / macOS 서명·공증 — 외부 전제 차단: Apple Developer Program($99/년) 가입 필요. 시뮬레이터 시동·페어링·큐 조회까지는 실증 완료. 가입 즉시 실행 절차는
DEPLOYMENT_PLAN.md§7.5 런북 - 리포지토리 GitHub 공개(D1 후반) — 사용자 결정 사항. 보안 전제 점검 통과(2026-08-16:
.env히스토리 커밋 0건·ignore 유효·템플릿만 추적) — 결정 즉시DEPLOYMENT_PLAN.md§7.5 의 2줄 명령으로 공개+첫 릴리스 가능
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)
dotnet build전체 0 오류 — 8개 프로젝트(App/Avalonia/Browser/Cli/Core/Mobile/Server/Tests)- 기존 미디어 감지·다운로드 회귀 없음 — Detection 이동 후 단위/E2E GREEN
- 폰 브라우저 웹 컴패니언 큐 조회/추가 (M0) — 서버 E2E
- Android 앱 큐 실시간 갱신/제어 (M1) — 에뮬 E2E 라이브 데이터
- 라이브러리 스트리밍 재생(시크 포함) (M2) — 실측 MP4 프레임 + 206 Range
- iOS 빌드 시동 + 큐 조회 (M3) — iOS 26 시뮬레이터 UI 렌더
- 폰 저장 오프라인 재생 (M2 후반) — 서버 정지 상태 프레임 렌더 실증 + 이어보기 동기화(재개 35.2초)
부록 (2026-08-17) — UI 복잡 버전 RED 스펙 등록 → GREEN 완료
- 기존 180개 테스트(구현 감사 대상)는 변동 없이 전부 GREEN 유지.
- 신규 234건 유즈케이스 스펙(
Tests/E2E/UiUsecase*RedTests.cs+docs/UI_USECASES.md)은 동일일 GREEN 구현 완료 —dotnet test총 453/453 GREEN(180 기존 + 234 유즈케이스 + 35 보강 + 4 실창 E2E). - RED 중 7건이 지목한 실측 스타일 결함(WCAG 대비 미달)은 색값 조정으로 해결: 다크 TextMuted #7C8296→#8B91A6(4.42→5.39:1)·AccentBrush #7C6CFF→#8B7CFF(4.38→5.17:1)· BorderBrush #343442→#6C6C8A(1.38→3.34:1), 라이트 TextMuted #767B8C→#63687A·TrackBrush #DBDFE6→#808798.
- 주요 구현: 디자인 토큰 체계(Shared.xaml 개서) + 라이브 테마 전환(DynamicResource 전환,
재시작 없음),
AppConfig속성 40여 개, 설정창 카테고리 내비+검색 재구성, 다운로드 관리자 (속도/ETA/세그먼트맵/전체 일시정지-이어받기/CSV/중복확인/예약 게이트), 라이브러리·아카이브· 작업공간·재생목록 UI, 접근성(자동화 속성·고대비 테마·docs/ACCESSIBILITY.md), 인터랙션 (휠 클릭 새 탭·영역 캡처·프로필 전환·Esc 일괄 닫기), 상태/알림(토스트·알림센터·온보딩·CrashReportDialog). - 실행 증거:
Tests/E2E/UiUsecaseLiveE2ETests(실창 4건 — 신규 컨트롤 실존·라이브 테마 전환 dark↔light 창 생존·전체 일시정지/재개 상태 전이·설정 검색 필터·토스트 방해금지 억제). 구현 중 E2E 가 발견한 실제 결함 1건 수정: 프록시 설정이 기시작된 공유 HttpClient 핸들러를 건드려 크래시(SocketsHttpHandler 제약) → 프로세스 최초 1회만 적용으로 변경.