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

@ -0,0 +1,115 @@
using System;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Xunit;
namespace Paca.Tests.Mobile;
/// <summary>
/// 모바일 안드로이드 최신 버전 지원(minSdk 28+, targetSdk 36) 및
/// Obsolete API(DisplayAlert, Device.StartTimer, Frame 등) 퇴출 기계적 감사 게이트
/// </summary>
public class MobileAndroidModernizationTests
{
private static string MobileDir => Path.Combine(SourceTree.Root, "src", "Paca.Mobile");
private static string AndroidManifestPath => Path.Combine(MobileDir, "Platforms", "Android", "AndroidManifest.xml");
private static string MobileCsprojPath => Path.Combine(MobileDir, "Paca.Mobile.csproj");
[Fact]
public void Gate_Android_MinSdkVersion_Is_At_Least_28_And_Target_Is_36()
{
// 1. AndroidManifest.xml 검증
Assert.True(File.Exists(AndroidManifestPath), "AndroidManifest.xml 이 존재해야 합니다.");
var manifestDoc = XDocument.Load(AndroidManifestPath);
var usesSdk = manifestDoc.Descendants("uses-sdk").FirstOrDefault();
Assert.NotNull(usesSdk);
XNamespace androidNs = "http://schemas.android.com/apk/res/android";
var minSdk = (int?)usesSdk.Attribute(androidNs + "minSdkVersion");
var targetSdk = (int?)usesSdk.Attribute(androidNs + "targetSdkVersion");
Assert.NotNull(minSdk);
Assert.True(minSdk.Value >= 28, $"minSdkVersion은 최소 28(Android 9.0) 이상이어야 합니다. 현재: {minSdk.Value}");
Assert.NotNull(targetSdk);
Assert.True(targetSdk.Value >= 36, $"targetSdkVersion은 최신 36(Android 16) 이상이어야 합니다. 현재: {targetSdk.Value}");
// 2. Paca.Mobile.csproj의 SupportedOSPlatformVersion 검증
Assert.True(File.Exists(MobileCsprojPath), "Paca.Mobile.csproj 가 존재해야 합니다.");
var csprojContent = File.ReadAllText(MobileCsprojPath);
Assert.DoesNotContain("<SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'\">24.0</SupportedOSPlatformVersion>", csprojContent);
Assert.Contains("<SupportedOSPlatformVersion Condition=\"$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'\">28.0</SupportedOSPlatformVersion>", csprojContent);
}
[Fact]
public void Gate_Mobile_Packages_Are_Modernized()
{
var csprojContent = File.ReadAllText(MobileCsprojPath);
// 1. ZXing.Net.Maui.Controls 최신화 및 넷6 구형 패키지(0.4.0) 퇴출
Assert.DoesNotContain("Include=\"ZXing.Net.Maui.Controls\" Version=\"0.4.0\"", csprojContent);
Assert.Contains("Include=\"ZXing.Net.Maui.Controls\" Version=\"0.10.4\"", csprojContent);
// 2. CommunityToolkit.Maui.MediaElement 최신화(.NET 10 대응 10.0.0) 및 4.0.0 퇴출
Assert.DoesNotContain("Include=\"CommunityToolkit.Maui.MediaElement\" Version=\"4.0.0\"", csprojContent);
Assert.Contains("Include=\"CommunityToolkit.Maui.MediaElement\" Version=\"10.0.0\"", csprojContent);
// 3. Plugin.LocalNotification 최신화
Assert.DoesNotContain("Include=\"Plugin.LocalNotification\" Version=\"11.1.0\"", csprojContent);
Assert.Contains("Include=\"Plugin.LocalNotification\" Version=\"14.1.1\"", csprojContent);
}
[Fact]
public void Gate_Mobile_No_Obsolete_Maui_Apis_Audit()
{
var csFiles = Directory.GetFiles(MobileDir, "*.cs", SearchOption.AllDirectories)
.Where(f => !f.Contains("\\obj\\") && !f.Contains("/obj/") && !f.Contains("\\bin\\") && !f.Contains("/bin/"))
.ToList();
foreach (var file in csFiles)
{
var fileName = Path.GetFileName(file);
var lines = File.ReadAllLines(file);
for (int i = 0; i < lines.Length; i++)
{
var line = lines[i];
var trimmed = line.Trim();
// 주석 라인은 제외
if (trimmed.StartsWith("//") || trimmed.StartsWith("/*") || trimmed.StartsWith("*"))
continue;
// 1. DisplayAlert( -> DisplayAlertAsync 사용 필수
if (line.Contains("DisplayAlert(") && !line.Contains("DisplayAlertAsync"))
{
Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'DisplayAlert' 가 발견되었습니다. 'DisplayAlertAsync'를 사용하세요: {trimmed}");
}
// 2. DisplayActionSheet( -> DisplayActionSheetAsync 사용 필수
if (line.Contains("DisplayActionSheet(") && !line.Contains("DisplayActionSheetAsync"))
{
Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'DisplayActionSheet' 가 발견되었습니다. 'DisplayActionSheetAsync'를 사용하세요: {trimmed}");
}
// 3. Device.StartTimer 퇴출
if (line.Contains("Device.StartTimer"))
{
Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'Device.StartTimer' 가 발견되었습니다. 'Dispatcher.StartTimer'를 사용하세요: {trimmed}");
}
// 4. new Frame 퇴출 (Android FrameLayout 레이아웃 제외 MAUI Frame 컨트롤)
if (line.Contains("new Frame") && !line.Contains("FrameLayout"))
{
Assert.Fail($"[{fileName}:{i + 1}] Obsolete API 'Frame' 이 발견되었습니다. 'Border'를 사용하세요: {trimmed}");
}
// 5. MainPage 직접 탐색 퇴출 (Application.Current.MainPage)
if (line.Contains(".MainPage"))
{
Assert.Fail($"[{fileName}:{i + 1}] Deprecated API 'MainPage' 가 발견되었습니다. Windows[0].Page 를 사용하세요: {trimmed}");
}
}
}
}
}