fix: allow safe fresh-clone DryRun builds

This commit is contained in:
2026-07-24 14:03:29 +09:00
parent b050f2f06c
commit 68a41194c9
15 changed files with 420 additions and 40 deletions

View File

@@ -44,10 +44,19 @@ public partial class App : Application
#else
const bool isDebugBuild = false;
#endif
var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply(
DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
#if SOURCE_ONLY_RUNTIME
const bool isSourceOnlyBuild = true;
#else
const bool isSourceOnlyBuild = false;
#endif
var launchArguments = isSourceOnlyBuild
? null
: DevelopmentLiveLaunchBootstrap.ResolveLaunchArguments(
args.Arguments,
Environment.GetCommandLineArgs()),
Environment.GetCommandLineArgs());
var developmentLive = DevelopmentLiveLaunchBootstrap.TryApply(
launchArguments,
isDebugBuild);
System.Diagnostics.Debug.WriteLine(
$"Development Live bootstrap: {developmentLive.Code}");

View File

@@ -26,12 +26,45 @@
<Version>0.1.0</Version>
<!--
Keep the legacy runtime assets outside this repository while reproducing the
original executable-folder layout. Override this property at build time when
the read-only MBN_STOCK_N checkout is in a different location:
original executable-folder layout. A fresh source checkout builds in safe
source-only DryRun mode when the sibling runtime tree is absent. Supplying an
explicit root or creating a Release MSIX makes the assets required unless the
caller explicitly selects Auto mode:
/p:LegacyRuntimeSourceRoot="D:\path\to\MBN_STOCK_N\bin\Debug"
/p:LegacyRuntimeAssetsMode=Auto
-->
<LegacyRuntimeSourceRootWasSpecified
Condition="'$(LegacyRuntimeSourceRoot)' != ''">true</LegacyRuntimeSourceRootWasSpecified>
<LegacyRuntimeSourceRoot Condition="'$(LegacyRuntimeSourceRoot)' == ''">$(MSBuildProjectDirectory)\..\..\..\MBN_STOCK_N\MBN_STOCK_N\bin\Debug</LegacyRuntimeSourceRoot>
<LegacyRuntimeSourceRoot>$([System.IO.Path]::GetFullPath('$(LegacyRuntimeSourceRoot)'))</LegacyRuntimeSourceRoot>
<LegacyRuntimeAssetsMode
Condition="'$(LegacyRuntimeAssetsMode)' == '' and
('$(LegacyRuntimeSourceRootWasSpecified)' == 'true' or
('$(Configuration)' == 'Release' and
('$(GenerateAppxPackageOnBuild)' == 'true' or
'$(PublishAppxPackage)' == 'true')))">Required</LegacyRuntimeAssetsMode>
<LegacyRuntimeAssetsMode
Condition="'$(LegacyRuntimeAssetsMode)' == ''">Auto</LegacyRuntimeAssetsMode>
<LegacyRuntimeAssetsEnabled
Condition="Exists('$(LegacyRuntimeSourceRoot)')">true</LegacyRuntimeAssetsEnabled>
<LegacyRuntimeAssetsEnabled
Condition="'$(LegacyRuntimeAssetsEnabled)' == ''">false</LegacyRuntimeAssetsEnabled>
<!--
Never share an executable directory with a previous full-runtime build.
Otherwise omitted Cuts/Res (including the local DB INI) could remain stale
and be consumed by a nominal source-only launch.
-->
<BaseOutputPath
Condition="'$(LegacyRuntimeAssetsEnabled)' != 'true'">bin\SourceOnly\</BaseOutputPath>
<IntermediateOutputPath
Condition="'$(LegacyRuntimeAssetsEnabled)' != 'true'">obj\SourceOnly\$(Platform)\$(Configuration)\</IntermediateOutputPath>
<DefineConstants
Condition="'$(LegacyRuntimeAssetsEnabled)' != 'true'">$(DefineConstants);SOURCE_ONLY_RUNTIME</DefineConstants>
<LegacyRuntimeAssetsValidationEnabled
Condition="'$(LegacyRuntimeAssetsMode)' == 'Required' or
'$(LegacyRuntimeAssetsEnabled)' == 'true'">true</LegacyRuntimeAssetsValidationEnabled>
<LegacyRuntimeAssetsValidationEnabled
Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == ''">false</LegacyRuntimeAssetsValidationEnabled>
<LegacyCutsSourceRoot>$(LegacyRuntimeSourceRoot)\Cuts</LegacyCutsSourceRoot>
<LegacyResSourceRoot>$(LegacyRuntimeSourceRoot)\Res</LegacyResSourceRoot>
</PropertyGroup>
@@ -40,7 +73,9 @@
<!-- Closed runtime contract: every active alias and non-external scene asset must exist. -->
<LegacyRequiredScene Include="5001;N5001;5006;5011;5016;50160;5023;5024;5025;5026;5029;8018;8032;5032;5037;5074;5076;5077;5078;5079;5080;5081;5082;5083;5084;5085;5086;50860;5087;5088;6001;6067;8001;8002;8003;8035;8061;8040;8046;8051;8056;8067;5068;5070;5072" />
<LegacyRequiredBuiltInAsset Include="images\주유기merge.png;images\35752913_l.jpg;Images\그림_빨강.png;Images\그림_검정.png;Images\그림_파랑.png;Images\프리마켓.png;Images\애프터마켓.png;Images\KRX.png;Images\NXT.png" />
</ItemGroup>
<ItemGroup Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'">
<!-- Explicit allowlist: these legacy Res assets contain UI/menu data only. -->
<LegacyPackagedResAsset Include="$(LegacyResSourceRoot)\a.png" />
<LegacyPackagedResAsset Include="$(LegacyResSourceRoot)\aa.png" />
@@ -88,7 +123,9 @@
<LegacyFinancialPreviewAsset Include="$(LegacyResSourceRoot)\profit.bmp" />
<!-- Official legacy wordmark, projected into the trusted Web root without a remote dependency. -->
<LegacyBrandAsset Include="$(LegacyResSourceRoot)\logo.png" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\..\LegacySceneRuntimeFactory.cs"
Link="Runtime\LegacySceneRuntimeFactory.cs" />
<Content Include="..\..\Assets\LockScreenLogo.scale-200.png" Link="Assets\LockScreenLogo.scale-200.png" />
@@ -111,25 +148,29 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="$(LegacyCutsSourceRoot)\**\*">
<Content Include="$(LegacyCutsSourceRoot)\**\*"
Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'">
<Link>Cuts\%(RecursiveDir)%(Filename)%(Extension)</Link>
<TargetPath>Cuts\%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="@(LegacyPackagedResAsset)">
<Content Include="@(LegacyPackagedResAsset)"
Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'">
<Link>Res\%(Filename)%(Extension)</Link>
<TargetPath>Res\%(Filename)%(Extension)</TargetPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="@(LegacyFinancialPreviewAsset)">
<Content Include="@(LegacyFinancialPreviewAsset)"
Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'">
<Link>Web\Previews\%(Filename)%(Extension)</Link>
<TargetPath>Web\Previews\%(Filename)%(Extension)</TargetPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
<Content Include="@(LegacyBrandAsset)">
<Content Include="@(LegacyBrandAsset)"
Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'">
<Link>Web\Brand\%(Filename)%(Extension)</Link>
<TargetPath>Web\Brand\%(Filename)%(Extension)</TargetPath>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -144,38 +185,59 @@
</ItemGroup>
<Target Name="ValidateLegacyRuntimeAssets" BeforeTargets="PrepareForBuild">
<Error Condition="!Exists('$(LegacyRuntimeSourceRoot)')"
Text="Legacy runtime source root was not found: $(LegacyRuntimeSourceRoot). Set /p:LegacyRuntimeSourceRoot to the read-only MBN_STOCK_N bin\Debug directory." />
<Error Condition="!Exists('$(LegacyCutsSourceRoot)')"
<Error Condition="'$(LegacyRuntimeAssetsMode)' != 'Auto' and
'$(LegacyRuntimeAssetsMode)' != 'Required'"
Text="LegacyRuntimeAssetsMode must be Auto or Required." />
<Message Condition="'$(LegacyRuntimeAssetsMode)' == 'Auto' and
'$(LegacyRuntimeAssetsEnabled)' != 'true'"
Importance="high"
Text="Legacy runtime assets were not found. Building the safe source-only DryRun app. Full runtime use requires a Required build with the approved external root." />
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyRuntimeSourceRoot)')"
Text="Legacy runtime source root was not found: $(LegacyRuntimeSourceRoot). Set /p:LegacyRuntimeSourceRoot to the read-only MBN_STOCK_N bin\Debug directory, or use /p:LegacyRuntimeAssetsMode=Auto for a source-only DryRun build." />
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyCutsSourceRoot)')"
Text="Legacy Cuts source directory was not found: $(LegacyCutsSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)')"
Text="Legacy Res source directory was not found: $(LegacyResSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\pie.bmp') or
!Exists('$(LegacyResSourceRoot)\Grow.bmp') or
!Exists('$(LegacyResSourceRoot)\sell.bmp') or
!Exists('$(LegacyResSourceRoot)\profit.bmp')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
(!Exists('$(LegacyResSourceRoot)\pie.bmp') or
!Exists('$(LegacyResSourceRoot)\Grow.bmp') or
!Exists('$(LegacyResSourceRoot)\sell.bmp') or
!Exists('$(LegacyResSourceRoot)\profit.bmp'))"
Text="The four required GraphE preview bitmaps are missing from $(LegacyResSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\logo.png')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)\logo.png')"
Text="The required legacy brand wordmark is missing: Res\logo.png." />
<Error Condition="!Exists('$(LegacyCutsSourceRoot)\%(LegacyRequiredScene.Identity).t2s')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyCutsSourceRoot)\%(LegacyRequiredScene.Identity).t2s')"
Text="A required active legacy scene file is missing: Cuts\%(LegacyRequiredScene.Identity).t2s." />
<Error Condition="!Exists('$(LegacyCutsSourceRoot)\%(LegacyRequiredBuiltInAsset.Identity)')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyCutsSourceRoot)\%(LegacyRequiredBuiltInAsset.Identity)')"
Text="A required built-in legacy scene asset is missing: Cuts\%(LegacyRequiredBuiltInAsset.Identity)." />
<Error Condition="'$(GenerateAppxPackageOnBuild)' != 'true' and
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
'$(GenerateAppxPackageOnBuild)' != 'true' and
'$(PublishAppxPackage)' != 'true' and
!Exists('$(LegacyResSourceRoot)\MmoneyCoder.ini')"
Text="The required legacy database settings file Res\MmoneyCoder.ini is missing from $(LegacyRuntimeSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\종목.ini')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)\종목.ini')"
Text="The required legacy stock menu file Res\종목.ini is missing from $(LegacyRuntimeSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\업종_코스피.ini')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)\업종_코스피.ini')"
Text="The required legacy KOSPI industry menu file Res\업종_코스피.ini is missing from $(LegacyRuntimeSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\업종_코스닥.ini')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)\업종_코스닥.ini')"
Text="The required legacy KOSDAQ industry menu file Res\업종_코스닥.ini is missing from $(LegacyRuntimeSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\해외.ini')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)\해외.ini')"
Text="The required legacy overseas menu file Res\해외.ini is missing from $(LegacyRuntimeSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\환율.ini')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)\환율.ini')"
Text="The required legacy exchange menu file Res\환율.ini is missing from $(LegacyRuntimeSourceRoot)." />
<Error Condition="!Exists('$(LegacyResSourceRoot)\지수.ini')"
<Error Condition="'$(LegacyRuntimeAssetsValidationEnabled)' == 'true' and
!Exists('$(LegacyResSourceRoot)\지수.ini')"
Text="The required legacy index menu file Res\지수.ini is missing from $(LegacyRuntimeSourceRoot)." />
</Target>
@@ -187,7 +249,7 @@
executable-relative layout only for ordinary local output. The CPS-only
up-to-date items below are not package payload candidates.
-->
<ItemGroup>
<ItemGroup Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true'">
<!-- These CPS-only items participate in Visual Studio's fast up-to-date check. -->
<UpToDateCheckInput Include="$(LegacyResSourceRoot)\MmoneyCoder.ini" />
<UpToDateCheckBuilt Include="$(TargetDir)Res\MmoneyCoder.ini" />
@@ -197,7 +259,9 @@
AfterTargets="Build"
Inputs="$(LegacyResSourceRoot)\MmoneyCoder.ini"
Outputs="$(TargetDir)Res\MmoneyCoder.ini"
Condition="'$(GenerateAppxPackageOnBuild)' != 'true' and '$(PublishAppxPackage)' != 'true'">
Condition="'$(LegacyRuntimeAssetsEnabled)' == 'true' and
'$(GenerateAppxPackageOnBuild)' != 'true' and
'$(PublishAppxPackage)' != 'true'">
<MakeDir Directories="$(TargetDir)Res" />
<Copy SourceFiles="$(LegacyResSourceRoot)\MmoneyCoder.ini"
DestinationFiles="$(TargetDir)Res\MmoneyCoder.ini"

View File

@@ -36,7 +36,8 @@ public sealed partial class MainWindow
PlayoutOptionsLoader.DefaultPath);
_playoutOptions = PlayoutOptionsLoader.Load(
operatorSceneDirectory: _appliedOperatorSettings.SceneDirectory,
operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory);
operatorBackgroundDirectory: _appliedOperatorSettings.BackgroundDirectory,
forceSafeDryRun: IsSourceOnlyBuild);
ResetRefreshState();
var startupComposition = LegacyParityStartupCompositionResolver.Resolve(
_playoutOptions,

View File

@@ -22,6 +22,18 @@ public sealed partial class MainWindow : Window
private const uint WmDpiChanged = 0x02E0;
private const nuint InteractionMetricsSubclassId = 0x4D424E49;
private static bool IsSourceOnlyBuild
{
get
{
#if SOURCE_ONLY_RUNTIME
return true;
#else
return false;
#endif
}
}
private readonly CancellationTokenSource _lifetimeCancellation = new();
private readonly SemaphoreSlim _intentGate = new(1, 1);
private readonly SemaphoreSlim _orderedLocalSelectionIntentGate = new(1, 1);
@@ -101,6 +113,12 @@ public sealed partial class MainWindow : Window
string? initializationError = null;
try
{
if (IsSourceOnlyBuild)
{
throw new InvalidOperationException(
"Source-only builds do not initialize a database runtime.");
}
// A Gate A plan pins database.local.json by path, size and SHA-256.
// Never inspect either packaged or operator-selected legacy INI files
// during that one-shot validation process.
@@ -170,8 +188,9 @@ public sealed partial class MainWindow : Window
}
catch
{
initializationError =
"데이터베이스가 설정되지 않습니다. 로컬 설정을 확인하세요.";
initializationError = IsSourceOnlyBuild
? "소스 전용 DryRun에서는 데이터베이스 연결을 사용하지 않습니다."
: "데이터베이스가 설정되지 않았습니다. 로컬 설정을 확인하세요.";
stockLookup = new UnavailableStockLookup(initializationError);
industrySelectionService = new UnavailableIndustrySelectionService(
initializationError);

View File

@@ -203,6 +203,7 @@
<header class="workspace-header">
<div class="brand">
<span class="brand-wordmark-plate">
<span class="brand-wordmark-fallback">매일경제TV</span>
<img class="brand-wordmark" src="Brand/logo.png" width="176" height="43"
alt="매일경제TV" draggable="false">
</span>

View File

@@ -945,6 +945,23 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--ui-accent); }
background: #fff;
box-shadow: 0 0 0 1px rgba(15, 23, 42, .08), 0 1px 2px rgba(15, 23, 42, .08);
}
.brand-wordmark-fallback,
.brand-wordmark {
grid-area: 1 / 1;
}
.brand-wordmark-fallback {
visibility: hidden;
color: #172033;
font-size: 16px;
font-weight: 800;
letter-spacing: -.045em;
}
.brand-wordmark-plate.wordmark-fallback-active .brand-wordmark-fallback {
visibility: visible;
}
.brand-wordmark-plate.wordmark-fallback-active .brand-wordmark {
display: none;
}
.brand-wordmark {
display: block;
width: 142px;

View File

@@ -17,6 +17,19 @@
const shell = document.querySelector(".operator-shell");
const splitter = document.getElementById("workspace-splitter");
const playlistPanel = document.querySelector(".playlist-panel");
const brandWordmark = document.querySelector("img.brand-wordmark");
const brandWordmarkPlate = brandWordmark?.closest(".brand-wordmark-plate");
function showBrandWordmarkFallback() {
brandWordmarkPlate?.classList.add("wordmark-fallback-active");
}
if (brandWordmark) {
brandWordmark.addEventListener("error", showBrandWordmarkFallback, { once: true });
if (brandWordmark.complete && brandWordmark.naturalWidth === 0) {
showBrandWordmarkFallback();
}
}
if (!region || !navigation || !navigationItems || !toggle || !toggleLabel ||
!stockTab || !settingsTab || !stockWorkspace || !catalogWorkspace ||

View File

@@ -22,8 +22,17 @@ public static class PlayoutOptionsLoader
public static PlayoutOptions Load(
string? path = null,
string? operatorSceneDirectory = null,
string? operatorBackgroundDirectory = null)
string? operatorBackgroundDirectory = null,
bool forceSafeDryRun = false)
{
// Source-only builds are for code/UI review on machines without the
// approved runtime tree. Do not consume a previous machine-local playout
// profile or process override in that build flavor.
if (forceSafeDryRun)
{
return new PlayoutOptions();
}
var options = LoadJson(path ?? DefaultPath);
ApplyOperatorAssetOverrides(
options,