diff --git a/.gitignore b/.gitignore
index a81a5ba..9b038a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,6 +42,8 @@ coverage*.json
*.log
positions.json
library.json
+site/ad_*.html
+
# ── 다운로드 테스트 결과물 (엔진 검증 중 루트에 떨어질 수 있음) ─────
*.mp4
diff --git a/build/installer.iss b/build/installer.iss
index 5458ecc..391f26c 100644
--- a/build/installer.iss
+++ b/build/installer.iss
@@ -62,7 +62,8 @@ Source: "{#PUBLISH_DIR}\*"; DestDir: "{app}"; Flags: recursesubdirs createallsub
[Icons]
Name: "{group}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}"
Name: "{group}\{#APP_NAME} 제거"; Filename: "{uninstallexe}"
-Name: "{commondesktop}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}"; Tasks: desktopicon
+Name: "{autodesktop}\{#APP_NAME}"; Filename: "{app}\{#APP_EXE}"; Tasks: desktopicon
+
[Run]
Filename: "{app}\{#APP_EXE}"; Description: "{cm:LaunchProgram,{#APP_NAME}}"; Flags: nowait postinstall skipifsilent
diff --git a/site/en/index.html b/site/en/index.html
index c5839f1..f7d8a14 100644
--- a/site/en/index.html
+++ b/site/en/index.html
@@ -32,7 +32,7 @@
"description": "A free media browser that detects HLS, DASH and MP4 streams the moment they play, inherits the browser login session, and saves lossless MP4 with 16-thread parallel resume.",
"url": "https://paca.chanpaca.net/en/",
"downloadUrl": "https://paca.chanpaca.net/releases.html",
- "softwareVersion": "1.3.0",
+ "softwareVersion": "1.3.1",
"datePublished": "2026-08-16",
"dateModified": "2026-09-19",
"inLanguage": "en",
diff --git a/site/index.html b/site/index.html
index 761111f..8e5a4d3 100644
--- a/site/index.html
+++ b/site/index.html
@@ -34,7 +34,7 @@
"description": "재생하는 순간 스트림(HLS·DASH·MP4)을 감지해 로그인 세션 그대로 저장하고, 휴대폰으로 이어보는 무료 미디어 브라우저.",
"url": "https://paca.chanpaca.net/",
"downloadUrl": "https://paca.chanpaca.net/releases.html",
- "softwareVersion": "1.3.0",
+ "softwareVersion": "1.3.1",
"offers": {
"@type": "Offer",
"price": "0",
diff --git a/src/Paca.App/App.xaml.cs b/src/Paca.App/App.xaml.cs
index fdf9496..ad8fc3f 100644
--- a/src/Paca.App/App.xaml.cs
+++ b/src/Paca.App/App.xaml.cs
@@ -52,6 +52,14 @@ public partial class App : Application
ApplyFontSettings();
try { File.AppendAllText(startupLog, "[Startup 6] ApplyReduceMotion\n"); } catch { }
ApplyReduceMotion(config.ReduceMotion);
+
+ // 실운영 구글 광고 모드 활성화 (ca-pub-1039714767792854 및 슬롯 4387520844/1521297311 실시간 송출)
+ Paca.Browser.Ads.DesktopAdManager.Instance.Config = Paca.Browser.Ads.DesktopAdConfig.PacaProduction with
+ {
+ ForceTestAds = false
+ };
+ Environment.SetEnvironmentVariable("PACADESKTOP_USE_PRODUCTION_ADS", "true");
+
try { File.AppendAllText(startupLog, "[Startup 7] new MainWindow()\n"); } catch { }
var mw = new MainWindow();
try { File.AppendAllText(startupLog, "[Startup 8] mw.Show()\n"); } catch { }
diff --git a/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs b/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs
index 299c565..28f94de 100644
--- a/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs
+++ b/src/Paca.App/Controls/DesktopAdBannerControl.xaml.cs
@@ -223,6 +223,14 @@ public partial class DesktopAdBannerControl : UserControl
}
}
+ if (_webView.CoreWebView2 != null)
+ {
+ // 표준 크롬 브라우저 User-Agent 설정으로 WebView 광고 억제 원천 방지
+ _webView.CoreWebView2.Settings.UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36";
+ _webView.CoreWebView2.Settings.AreDevToolsEnabled = false;
+ _webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
+ }
+
_webView.NavigationCompleted -= OnNavigationCompleted;
_webView.NavigationCompleted += OnNavigationCompleted;
_webView.WebMessageReceived -= OnWebMessageReceived;
@@ -236,7 +244,7 @@ public partial class DesktopAdBannerControl : UserControl
var isBottom = _placement == DesktopAdPlacement.BottomAppBanner;
var adWidth = isBottom ? 728 : 300;
var adHeight = isBottom ? 90 : 250;
- var divId = isBottom ? "div-gpt-ad-bottom" : "div-gpt-ad-banner";
+ var divId = isBottom ? "div-ad-bottom" : "div-ad-banner";
if (_webView != null)
{
@@ -246,12 +254,15 @@ public partial class DesktopAdBannerControl : UserControl
_webView.VerticalAlignment = VerticalAlignment.Center;
}
+ var isProd = !DesktopAdManager.Instance.Config.ForceTestAds;
var slotId = !string.IsNullOrWhiteSpace(_adData.AdUnitId)
? _adData.AdUnitId
: (isBottom ? DesktopAdManager.Instance.Config.EffectiveNtpBannerUnitId : DesktopAdManager.Instance.Config.EffectiveDownloadBannerUnitId);
var snippet = _adData.HtmlEmbedSnippet ??
- DesktopAdConfig.GenerateGoogleTagSnippet(slotId, adWidth, adHeight, divId);
+ (isProd
+ ? DesktopAdConfig.GenerateProductionAdSnippet(slotId, adWidth, adHeight, divId)
+ : DesktopAdConfig.GenerateGoogleTagSnippet(slotId, adWidth, adHeight, divId));
var pageHtml = GenerateRealAdHtml(_adData, _placement, snippet, adWidth, adHeight);
@@ -276,12 +287,18 @@ public partial class DesktopAdBannerControl : UserControl
PART_NativeCard.Visibility = Visibility.Collapsed;
var cacheBuster = DateTime.UtcNow.Ticks;
- var virtualUrl = $"https://ads.paca.cloud/ad_{_placement}.html?v={cacheBuster}";
+ var virtualUrl = $"https://mediabrowser.chanpaca.net/ad_{_placement}.html?v={cacheBuster}";
var fileUrl = new Uri(tempAdHtmlPath).AbsoluteUri + $"?v={cacheBuster}";
string targetUrl = virtualUrl;
try
{
+ // 실제 배포 도메인(ads.txt 검증 호스트) 및 레거시 ads.paca.cloud 가상 호스트 동시 매핑
+ _webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping(
+ "mediabrowser.chanpaca.net",
+ adDir,
+ Microsoft.Web.WebView2.Core.CoreWebView2HostResourceAccessKind.Allow);
+
_webView?.CoreWebView2?.SetVirtualHostNameToFolderMapping(
"ads.paca.cloud",
adDir,
@@ -332,7 +349,8 @@ public partial class DesktopAdBannerControl : UserControl
-
+
+
PACA Google Ad
";
+ }
+ ///
+ /// Google 공식 실시간 프로덕션 광고 스니펫 생성 (Google AdSense ca-pub-1039714767792854 및 슬롯 바인딩 + 실시간 트래킹)
+ ///
+ public static string GenerateProductionAdSnippet(string adUnitId, int width, int height, string divId = "div-real-ad-slot")
+ {
+ var cleanPub = AdClient;
+ var cleanSlot = !string.IsNullOrWhiteSpace(adUnitId) && adUnitId.Contains('/')
+ ? adUnitId.Substring(adUnitId.LastIndexOf('/') + 1)
+ : (!string.IsNullOrWhiteSpace(adUnitId) ? adUnitId : (width >= 700 ? "4387520844" : "1521297311"));
+
+ return $@"
+
+";
}
}
@@ -156,15 +197,24 @@ public sealed class DesktopAdManager
public event Action? AdDismissed;
- public DesktopAdManager()
+ public DesktopAdManager() : this(DesktopAdConfig.PacaProduction)
{
+ }
+
+ public DesktopAdManager(DesktopAdConfig config)
+ {
+ Config = config;
InitializeDefaultAds();
}
private void InitializeDefaultAds()
{
+ bool isProd = !Config.ForceTestAds;
+
// 1. 새 탭 페이지(NTP) Google 실시간 스폰서 슬롯 (728x90)
- var ntpSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-ntp");
+ var ntpSnippet = isProd
+ ? DesktopAdConfig.GenerateProductionAdSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-ad-ntp")
+ : DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-ntp");
var ntpAd = new DesktopAdData
{
Id = "paca-desktop-ntp-sponsor",
@@ -173,7 +223,7 @@ public sealed class DesktopAdManager
Badge = "AD",
Title = "Google Sponsor Network",
Description = "Google 공식 인증 파트너 실시간 디스플레이 광고",
- Advertiser = "Google Ad Manager",
+ Advertiser = isProd ? "Google AdSense" : "Google Ad Manager",
CtaText = "자세히 보기",
TargetUrl = "https://www.google.com/ads",
IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png",
@@ -183,7 +233,9 @@ public sealed class DesktopAdManager
};
// 2. 다운로드 뷰 배너 (300x250 Medium Rectangle)
- var dlSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveDownloadBannerUnitId, 300, 250, "div-gpt-ad-dl");
+ var dlSnippet = isProd
+ ? DesktopAdConfig.GenerateProductionAdSnippet(Config.EffectiveDownloadBannerUnitId, 300, 250, "div-ad-dl")
+ : DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveDownloadBannerUnitId, 300, 250, "div-gpt-ad-dl");
var dlAd = new DesktopAdData
{
Id = "paca-desktop-dl-sponsor",
@@ -192,7 +244,7 @@ public sealed class DesktopAdManager
Badge = "AD",
Title = "Google Display Network",
Description = "Google 공식 실시간 타깃 디스플레이 광고 슬롯",
- Advertiser = "Google Ad Manager",
+ Advertiser = isProd ? "Google AdSense" : "Google Ad Manager",
CtaText = "자세히 보기",
TargetUrl = "https://www.google.com/ads",
IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png",
@@ -217,7 +269,9 @@ public sealed class DesktopAdManager
};
// 4. 앱 메인 하단 고정 배너 (728x90 Leaderboard)
- var bottomSnippet = DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-bottom");
+ var bottomSnippet = isProd
+ ? DesktopAdConfig.GenerateProductionAdSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-ad-bottom")
+ : DesktopAdConfig.GenerateGoogleTagSnippet(Config.EffectiveNtpBannerUnitId, 728, 90, "div-gpt-ad-bottom");
var bottomAd = new DesktopAdData
{
Id = "paca-desktop-bottom-sponsor",
@@ -226,7 +280,7 @@ public sealed class DesktopAdManager
Badge = "AD",
Title = "Google Sponsor Network",
Description = "Google 공식 실시간 인증 디스플레이 광고 배너",
- Advertiser = "Google Ad Manager",
+ Advertiser = isProd ? "Google AdSense" : "Google Ad Manager",
CtaText = "자세히 보기",
TargetUrl = "https://www.google.com/ads",
IconOrImageUrl = "https://www.gstatic.com/images/branding/googlelogo/2x/googlelogo_color_160x56dp.png",
diff --git a/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs b/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs
index 3671d8c..13e2f0f 100644
--- a/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs
+++ b/tests/Paca.Tests/App/DesktopGatewayDaemonCommandLineGateTests.cs
@@ -62,4 +62,21 @@ public class DesktopGatewayDaemonCommandLineGateTests
Assert.Equal(52101, config.BeaconUdpPort);
Assert.True(config.AutoStartUdpBeacon);
}
+
+ [Fact]
+ public void Gate157_InstallerScript_MustUseAutoDesktop_And_MustNotContainCommonDesktop_ForPerUserSafety()
+ {
+ var installerPath = Path.Combine(SourceTree.Root, "build", "installer.iss");
+ Assert.True(File.Exists(installerPath), "installer.iss must exist");
+
+ var content = File.ReadAllText(installerPath);
+
+ // Standard user (PrivilegesRequired=lowest) has no write permission to {commondesktop} (C:\Users\Public\Desktop).
+ // Saving a shortcut to {commondesktop} throws IPersistFile::Save 0x80070005 (Access is Denied).
+ // It must use {autodesktop} which resolves to {userdesktop} for standard users.
+ Assert.DoesNotContain("{commondesktop}", content);
+ Assert.Contains("{autodesktop}\\{#APP_NAME}", content);
+ Assert.DoesNotContain("{commonprograms}", content);
+ }
}
+
diff --git a/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs b/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs
index 677925e..cef0df7 100644
--- a/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs
+++ b/tests/Paca.Tests/Browser/DesktopAdSlotAndNtpSponsoredGateTests.cs
@@ -425,6 +425,45 @@ public class DesktopAdSlotAndNtpSponsoredGateTests
Assert.Contains("data-ad-client=\"ca-pub-1039714767792854\"", adsenseSnippet);
Assert.Contains("data-ad-slot=\"4387520844\"", adsenseSnippet);
}
+
+ [Fact]
+ public void Gate214_RealGoogleAds_AdSense_ProductionSnippets_And_DomainOrigin_Audit()
+ {
+ // 1. 프로덕션 모드 DesktopAdManager 생성 시 실제 단위 슬롯이 주입되어야 함
+ var prodConfig = DesktopAdConfig.PacaProduction with { ForceTestAds = false };
+ var adManager = new DesktopAdManager(prodConfig);
+
+ var bottomAd = adManager.GetAdForPlacement(DesktopAdPlacement.BottomAppBanner);
+ Assert.NotNull(bottomAd);
+ Assert.True(bottomAd.IsRealAd);
+ Assert.Equal("ca-app-pub-1039714767792854/4387520844", bottomAd.AdUnitId);
+ Assert.NotNull(bottomAd.HtmlEmbedSnippet);
+ Assert.Contains("ca-pub-1039714767792854", bottomAd.HtmlEmbedSnippet);
+ Assert.Contains("4387520844", bottomAd.HtmlEmbedSnippet);
+ Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", bottomAd.HtmlEmbedSnippet);
+
+ var dlAd = adManager.GetAdForPlacement(DesktopAdPlacement.DownloadViewBanner);
+ Assert.NotNull(dlAd);
+ Assert.True(dlAd.IsRealAd);
+ Assert.Equal("ca-app-pub-1039714767792854/1521297311", dlAd.AdUnitId);
+ Assert.NotNull(dlAd.HtmlEmbedSnippet);
+ Assert.Contains("ca-pub-1039714767792854", dlAd.HtmlEmbedSnippet);
+ Assert.Contains("1521297311", dlAd.HtmlEmbedSnippet);
+ Assert.Contains("pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", dlAd.HtmlEmbedSnippet);
+
+ // 2. DesktopAdConfig.GenerateProductionAdSnippet 검증
+ var snippet = DesktopAdConfig.GenerateProductionAdSnippet("ca-app-pub-1039714767792854/4387520844", 728, 90, "div-bottom");
+ Assert.Contains("ca-pub-1039714767792854", snippet);
+ Assert.Contains("data-ad-slot=\"4387520844\"", snippet);
+ Assert.Contains("data-ad-status", snippet);
+
+ // 3. DesktopAdBannerControl.xaml.cs에서 실 도메인 mediabrowser.chanpaca.net 및 크롬 User-Agent 감사
+ var bannerCsPath = Path.Combine(AppDir, "Controls", "DesktopAdBannerControl.xaml.cs");
+ var bannerCs = File.ReadAllText(bannerCsPath);
+ Assert.Contains("mediabrowser.chanpaca.net", bannerCs);
+ Assert.Contains("Chrome/130", bannerCs);
+ Assert.Contains("Settings.UserAgent", bannerCs);
+ }
}