feat(ads/seo/release): v1.2.0 with live Google Ads, isolated ad profile, ads.txt, and SEO/AEO/GEO optimization

This commit is contained in:
Yun Chan 2026-09-14 00:39:44 +09:00
parent a88aafa1e5
commit 0e568f1f0a
975 changed files with 130593 additions and 16783 deletions

View file

@ -15,6 +15,10 @@ public class MobileUiProductionGateTests
? Path.Combine(SourceTree.Root, "src", "Paca.Mobile")
: Path.Combine(SourceTree.Root, "Paca.Mobile");
private static string MobileCoreProjectDir => Directory.Exists(Path.Combine(SourceTree.Root, "src", "Paca.Mobile.Core"))
? Path.Combine(SourceTree.Root, "src", "Paca.Mobile.Core")
: Path.Combine(SourceTree.Root, "Paca.Mobile.Core");
[Fact]
public void Gate93_TabbedRootPage_FiveTabs_Completeness_Audit()
{
@ -90,4 +94,266 @@ public class MobileUiProductionGateTests
Assert.True(File.Exists(appIconPath), "paca_app_icon.png must exist");
Assert.True(File.Exists(splashPath), "paca_splash_screen.png must exist");
}
[Fact]
public void Gate114_ConnectPage_LanAutoDiscovery_And_WanConnectCode_Ui_Audit()
{
var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs"));
Assert.Contains("주변 PC 자동 검색", pagesCs);
Assert.Contains("원클릭 연결", pagesCs);
Assert.Contains("ConnectCode", pagesCs);
Assert.Contains("3G / 4G / 5G", pagesCs);
}
[Fact]
public void Gate151_QueuePage_SmartClipboardBanner_Ui_Audit()
{
var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs"));
Assert.Contains("_clipboardBanner", pagesCs);
Assert.Contains("클립보드 감지", pagesCs);
Assert.Contains("PC로 다운로드", pagesCs);
}
[Fact]
public void Gate152_LibraryPage_OfflineStorageDashboard_Ui_Audit()
{
var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs"));
Assert.Contains("_storageProgressBar", pagesCs);
Assert.Contains("_storageSummaryLabel", pagesCs);
Assert.Contains("오프라인 보관함", pagesCs);
}
[Fact]
public void Gate160_ApiClient_AudioStreamUrl_Supported()
{
var targetFile = File.Exists(Path.Combine(MobileCoreProjectDir, "ApiClient.cs"))
? Path.Combine(MobileCoreProjectDir, "ApiClient.cs")
: Path.Combine(MobileProjectDir, "ApiClient.cs");
var apiClientCs = File.ReadAllText(targetFile);
Assert.Contains("AudioUrl", apiClientCs);
Assert.Contains("/api/audio/", apiClientCs);
}
[Fact]
public void Gate161_BrowserPage_BrandLogo_Asset_And_Source_Audit()
{
var logoAssetPath = Path.Combine(MobileProjectDir, "Resources", "Images", "paca_logo.png");
Assert.True(File.Exists(logoAssetPath), "paca_logo.png must exist in Resources/Images");
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.Contains("paca_logo.png", browserCs);
}
[Fact]
public void Gate162_BrowserPage_BottomScrollPadding_AvoidsClipping_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
// CreateNewTabPageView rootLayout bottom padding must be >= 100 to avoid clipping with bottom floating bar
var match = Regex.Match(browserCs, @"rootLayout[\s\S]*?Padding\s*=\s*new\s*Thickness\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*(\d+)\s*\)");
Assert.True(match.Success, "Padding definition must exist on rootLayout");
int bottomPadding = int.Parse(match.Groups[1].Value);
Assert.True(bottomPadding >= 100, $"Bottom padding must be at least 100 to clear floating bottom bar, but was {bottomPadding}");
}
[Fact]
public void Gate163_MobileApp_NoUnderlineEntry_Handler_Audit()
{
var mauiProgramCs = File.ReadAllText(Path.Combine(MobileProjectDir, "MauiProgram.cs"));
Assert.Contains("EntryHandler", mauiProgramCs);
Assert.Contains("Background", mauiProgramCs);
}
[Fact]
public void Gate164_BrowserPage_SpeedDials_Modern_Lucide_Glyph_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.Contains("MobileIconSystem.FontFamily", browserCs);
Assert.Contains("MobileIconSystem.Play", browserCs);
}
[Fact]
public void Gate165_AndroidManifest_WebView_PackageVisibility_Queries_Audit()
{
var manifestPath = Path.Combine(MobileProjectDir, "Platforms", "Android", "AndroidManifest.xml");
var manifestText = File.ReadAllText(manifestPath);
Assert.Contains("<queries>", manifestText);
Assert.Contains("com.google.android.webview", manifestText);
}
[Fact]
public void Gate166_BrowserPage_LazyWebView_FaultTolerant_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.Contains("private bool EnsureWebView()", browserCs);
Assert.Contains("if (!EnsureWebView()", browserCs);
}
[Fact]
public void Gate167_BrowserPage_SnifferBadge_EmeraldColor_And_Glyph_Audit()
{
// DownloadsPage 안내문구: '웹 브라우징 중 상단 에메랄드 비디오 뱃지를 눌러 고화질 영상을 다운로드하세요.'
// 에메랄드 컬러(#059669 또는 #10B981) 적용 및 비디오 글리프/텍스트 매핑 검증
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.True(browserCs.Contains("#059669") || browserCs.Contains("#10B981"),
"BrowserPage _snifferBadge는 DownloadsPage 안내문구와 100% 일치하는 에메랄드(#059669 또는 #10B981) 배경색이어야 합니다.");
}
[Fact]
public void Gate168_BrowserPage_DesktopModeBtn_Decoupled_From_PcSync_Audit()
{
// 데스크톱 모드 전환 버튼과 PC 연결 상태 인디케이터의 변수/로직 분리 감사
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.Contains("_desktopModeBtn", browserCs);
Assert.DoesNotContain("_pcStatusIndicator.TextColor = Color.FromArgb(\"#34D399\")", browserCs);
}
[Fact]
public void Gate171_BrowserPage_Sniffer_Detects_Supported_Platforms_Audit()
{
// 플랫폼 비디오(유튜브/치지직/숲 등) 진입 시 DOM blob 여부와 무관하게 플랫폼 비디오가 스니퍼 뱃지에 반영되는지 감사
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.Contains("MobileDiscoveryEngine.DetectPlatform", browserCs);
}
[Fact]
public void Gate172_BrowserPage_ShareAction_Invokes_Native_Share_Audit()
{
// 더보기 메뉴의 '링크 복사 및 공유' 클릭 시 클립보드 복사뿐 아니라 OS 네이티브 공유 시트(Share.Default.RequestAsync)를 연동하는지 감사
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.Contains("Share.Default.RequestAsync", browserCs);
}
[Fact]
public void Gate173_BrowserPage_Sniffer_Extracts_True_Url_Not_Source_ToString_Audit()
{
// MAUI UrlWebViewSource.ToString()은 클래스명을 반환하므로 반드시 window.location.href 또는 UrlWebViewSource.Url/e.Url을 사용하여 실제 URL을 추출해야 함
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
Assert.DoesNotContain("_webView.Source?.ToString() ?? e.Url", browserCs);
Assert.Contains("window.location.href", browserCs);
}
[Fact]
public void Gate174_SyncSettingsPage_LanDiscovery_Ui_And_AutoConnect_Pills_Audit()
{
var syncCs = File.ReadAllText(Path.Combine(MobileProjectDir, "SyncSettingsPage.cs"));
// 원클릭 LAN 자동 발견 프로버 연동 및 UI 구성요소 감사
Assert.Contains("LanDiscoveryProber", syncCs);
Assert.Contains("로컬 네트워크 탐색", syncCs);
Assert.Contains("10.0.2.2:5000", syncCs); // 에뮬레이터 퀵 페어링 지원
Assert.Contains("OnDeviceFound", syncCs);
}
[Fact]
public void Gate175_ApiClient_DirectHost_And_OptionalToken_Support_Audit()
{
var targetFile = File.Exists(Path.Combine(MobileCoreProjectDir, "ApiClient.cs"))
? Path.Combine(MobileCoreProjectDir, "ApiClient.cs")
: Path.Combine(MobileProjectDir, "ApiClient.cs");
var apiCs = File.ReadAllText(targetFile);
// 토큰 없는 로컬 LAN 직접 연결(10.0.2.2:5000, 192.168.x.x) 및 URI 정규화 지원 감사
Assert.Contains("lan_direct", apiCs);
Assert.Contains("!host.StartsWith(\"http", apiCs);
}
[Fact]
public void Gate176_MobileTabManager_SpeedDial_Deduplication_And_DomainMatching_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
// 바로가기(Speed Dial) 탭 시 중복 생성 방지 및 탭 전환 감사
Assert.Contains("FindTabByUrlOrHost", browserCs);
}
[Fact]
public void Gate177_MobileProject_16KbPageSize_And_AssemblyStore_Audit()
{
var csproj = File.ReadAllText(Path.Combine(MobileProjectDir, "Paca.Mobile.csproj"));
var manifest = File.ReadAllText(Path.Combine(MobileProjectDir, "Platforms", "Android", "AndroidManifest.xml"));
// 최신 안드로이드 15/16 16KB 페이지 사이즈 호환 및 어셈블리 스토어(.so 래핑 방지) 감사
Assert.True(csproj.Contains("<AndroidUseAssemblyStore") && csproj.Contains("true</AndroidUseAssemblyStore>"));
Assert.True(csproj.Contains("<AndroidEnable16KbPages") && csproj.Contains("true</AndroidEnable16KbPages>"));
Assert.Contains("android:targetSdkVersion=\"36\"", manifest);
}
[Fact]
public void Gate181_DownloadsPage_SingleRail_Compact_Filters_Audit()
{
var downloadsCs = File.ReadAllText(Path.Combine(MobileProjectDir, "DownloadsPage.cs"));
// 과밀한 4줄 버튼 대신 단일 레일 컴팩트 알약 스크롤 뷰 및 34dp 슬림 서치박스 감사
Assert.Contains("SingleRailPillsScrollView", downloadsCs);
Assert.Contains("FilterAll", downloadsCs);
Assert.Contains("FilterActive", downloadsCs);
Assert.Contains("FilterDone", downloadsCs);
Assert.Contains("FilterFailed", downloadsCs);
Assert.Contains("ClearFinishedDownloadsBtn", downloadsCs);
}
[Fact]
public void Gate182_QueuePage_CompactClipboardBanner_And_DoubleBezelCards_Audit()
{
var pagesCs = File.ReadAllText(Path.Combine(MobileProjectDir, "Pages.cs"));
// 비대한 48px 카드 대신 슬림한 클립보드 센서 알약 및 28px/32px 컴팩트 큐 컨트롤 감사
Assert.Contains("_clipboardBanner", pagesCs);
Assert.Contains("paca_empty_queue.png", pagesCs);
Assert.Contains("재시도", pagesCs);
Assert.Contains("CompactQueueCard", pagesCs);
}
[Fact]
public void Gate183_TabbedRootPage_Modern_FiveTabs_Integrity_Audit()
{
var rootCs = File.ReadAllText(Path.Combine(MobileProjectDir, "TabbedRootPage.cs"));
// 8개 탭 산발적 나열 대신 5대 핵심 탭 정렬 및 서브 탭 자연스러운 수용 감사
Assert.Contains("QueuePage", rootCs);
Assert.Contains("DiscoverPage", rootCs);
Assert.Contains("BrowserPage", rootCs);
Assert.Contains("FavoritesPage", rootCs);
Assert.Contains("SyncSettingsPage", rootCs);
Assert.Contains("Paca Mobile", rootCs);
}
[Fact]
public void Gate184_SpeedDial_TouchReliability_And_FullHitTest_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
// 스피드 다이얼 항목 터치 시 자식 요소(아이콘, 라벨 등)가 터치를 가로채지 않도록 InputTransparent 설정 감사
Assert.Contains("InputTransparent = true", browserCs);
Assert.Contains("CascadeInputTransparent = true", browserCs);
Assert.Contains("iconCircle.GestureRecognizers.Add(tap);", browserCs);
Assert.Contains("itemContainer.GestureRecognizers.Add(tap);", browserCs);
}
[Fact]
public void Gate185_Browser_Navigation_And_BackKeyHandling_Audit()
{
var browserCs = File.ReadAllText(Path.Combine(MobileProjectDir, "BrowserPage.cs"));
var tabbedRootCs = File.ReadAllText(Path.Combine(MobileProjectDir, "TabbedRootPage.cs"));
// 1. _isNavigating 락으로 인한 터치 무시/잠금 버그 완전 배제
Assert.DoesNotContain("_isNavigating", browserCs);
// 2. UI 스레드 안전 비동기 전환 보증
Assert.Contains("MainThread.BeginInvokeOnMainThread", browserCs);
// 3. 브라우저 페이지 및 탭 루트의 뒤로가기 제어 감사
Assert.Contains("public bool HandleBackButton()", browserCs);
Assert.Contains("browserPage.HandleBackButton()", tabbedRootCs);
}
[Fact]
public void Gate239_ApiClient_AutoRestore_Constructor_Audit()
{
var targetFile = File.Exists(Path.Combine(MobileCoreProjectDir, "ApiClient.cs"))
? Path.Combine(MobileCoreProjectDir, "ApiClient.cs")
: Path.Combine(MobileProjectDir, "ApiClient.cs");
var apiCs = File.ReadAllText(targetFile);
// ApiClient의 순수 생성자 및 영속 상태 복원 Restore() 메서드 계약 감사
Assert.Contains("public ApiClient(", apiCs);
Assert.Contains("public void Restore()", apiCs);
}
}