diff --git a/Directory.Build.props b/Directory.Build.props
index 97e397d..940e9c3 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -11,9 +11,9 @@
diff --git a/README.md b/README.md
index 49d3a3e..e2a8bb6 100644
--- a/README.md
+++ b/README.md
@@ -64,12 +64,9 @@ Oracle/MariaDB 조회 계층, 원본 10초 DB 상태 감시, Tornado/K3D 어댑
4. 실행 프로필이
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`인지 확인합니다.
5. `F5`로 빌드·배포·실행합니다.
-6. 첫 실행 설정 창에서 기존 코더의 `Cuts` 폴더와 여러 INI가 함께 있는 `Res` 폴더, 두
- 폴더만 각각 선택하고 `설정 시작`을 누릅니다.
-7. 이 앱의 이전 로컬 설정이 다른 경우에만 업데이트 확인 창이 한 번 표시됩니다. 선택한
- 두 폴더와 기본 Tornado2 설정으로 갱신하려면 `업데이트`를 누릅니다. K3D 기준값은 이
- 확인으로 바뀌지 않습니다.
-8. `설정 완료`가 표시되면 창을 닫고 `F5`를 한 번 더 누릅니다.
+6. 첫 실행 설정 창에서 기존 코더의 `Cuts` 폴더와 `Res` 폴더, 두 폴더만 각각 선택하고
+ `설정 시작`을 누릅니다.
+7. `설정 완료`가 표시되면 창을 닫고 `F5`를 한 번 더 누릅니다.
새로 받은 clone에 아직 이 PC의 검증된 runtime 연결이 없으면, 주변에 과거
`MBN_STOCK_N\...\bin\Debug`가 있더라도 첫 F5는 안전한
@@ -86,52 +83,36 @@ Tornado2/PGM에 연결하거나 명령을 보내지 않습니다. 출력과 중
필요가 없습니다. 첫 실행 창에는 다음 두 폴더만 입력합니다.
- `Cuts`: 실제 실행 자산의 `Cuts` 폴더
-- `설정/INI (Res)`: `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`,
- `업종_코스닥.ini`, `해외.ini`, `환율.ini`, `지수.ini`, `종목비교.ini`가 함께 있는
- 실제 `Res` 폴더
+- `설정/INI (Res)`: 기존 코더가 사용하는 실제 `Res` 폴더
-개별 INI를 따로 고르는 입력은 없습니다. 첫 실행 설정이 위 **활성 INI 8개**를 모두
-확인하며, `MmoneyCoder.ini`는 DB 설정으로, 나머지 7개는 컷/UI 구성 정보로 처리합니다.
+첫 실행 설정은 두 폴더 안의 파일을 열거·파싱·복사하지 않습니다. 폴더 경로만
+`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json`에 저장하고,
+같은 공통 부모를 Git 제외 `Directory.Build.local.props`에 기록합니다.
두 폴더는 같은 공통 부모 아래에 나란히 있어야 하며 폴더 이름도 각각 `Cuts`, `Res`여야
합니다. 소스 트리의 오래된 `RES`나 이름이 비슷한 백업 폴더가 아니라 기존 코더가 실제
실행에 사용하던 `bin\Debug\Cuts`, `bin\Debug\Res` 성격의 폴더를 선택합니다.
-초기화기는 기존 자산을 검증한 뒤 사용자 전용 LocalAppData에 검증된 runtime 복사본을 만들고,
-그 경로를 명시해 전체 Debug 빌드를 먼저 검증합니다. Git 제외
-`Directory.Build.local.props`는 DB/K3D/Development Live 설정까지 성공한 뒤 마지막 성공
-표식으로 기록되어, 중간에 Visual Studio가 종료되면 다음 F5도 첫 실행 설정 화면으로
-돌아옵니다.
-DB INI도 사용자 전용 LocalAppData overlay로 복사해 ACL을 보호하고, K3D x64 등록을 읽기
-전용으로 점검합니다. 예상 HKLM x64 등록·CLSID·ProgID·TypeLib·vendor 배치가 모두 맞는
-설치본의 두 DLL 해시를 이 PC의 최초 기준값으로 자동 고정하고, 이후 파일이 바뀌면
-Development Live를 차단합니다. 기준값은 사용자 전용
-`%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json`에 지속 저장되며 Git에는
-들어가지 않습니다. 지정 개발 환경의 Tornado2는 별도 입력 없이 회사 기본값
-`127.0.0.1:30001`로 구성합니다. 보호된 Development Live 설정은 모든 검증이 성공한
-마지막 기능 설정 단계에서만 발급되고, 그 뒤 로컬 build binding이 성공 표식으로 기록됩니다.
+`설정 시작`은 PowerShell, `dotnet build`, DB 복사, K3D 검사 또는 Tornado2/PGM 작업을
+실행하지 않으므로 즉시 완료되어야 합니다. 전체 앱 빌드는 창을 닫고 누르는 다음 F5에서
+진행됩니다. 이때도 `Cuts`는 복사하지 않고 선택한 원래 경로를 직접 사용합니다.
-테스트 개발 서버라 자격증명 노출 위험을 수용하는 경우에도 저장소 계약은 바뀌지 않습니다.
-`MmoneyCoder.ini`와 DB 자격증명은 사용자 전용 LocalAppData에만 두고 Git, 빌드 출력,
-runtime bundle과 MSIX에는 포함하지 않습니다.
-
-Debug x64 빌드 출력은 선택한 manifest와 경로·길이·SHA-256·파일 집합이 정확히 일치하고
-SourceOnly가 아닌지까지 자동 확인합니다. 중간 단계가 실패하면 기존 runtime 연결과 DB
-overlay를 원래 내용·ACL로 되돌리고 Live 승인 파일을 남기지 않습니다. 이 과정은 Tornado에
-`CONNECT`하거나 장면을 송출하지 않습니다.
+Development Live용 DB overlay, K3D pin과 한 회차 승인 파일이 아직 없는 PC는 별도의
+`Initialize-ExistingDevelopmentPc.ps1` 수동 절차를 완료하기 전까지 안전한 `DryRun`으로
+시작합니다. 첫 실행 폴더 저장은 이 보호 설정을 만들거나 변경하지 않습니다.
초기화가 성공하면 Visual Studio에서 `Debug|x64`, `Legacy Parity App (VS F5)`,
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작합니다.
-앱이나 빌드 출력이 `SourceOnly` 또는 `DryRun`이라고 표시되면 송출을 시도하지 말고 초기화를
-바로잡습니다. 최초 실제 확인은 [개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md)에 따라
+앱이나 빌드 출력이 `SourceOnly` 또는 `DryRun`이라고 표시되면 송출을 시도하지 말고 별도
+Development Live 준비 상태를 확인합니다. 최초 실제 확인은 [개발 PGM 인수 절차](docs/DEVELOPMENT_LIVE_HANDOFF.md)에 따라
`5001` 하나로 진행합니다. Development Live는 이 Debug 프로세스에만 적용되며 Release와 기본
프로필은 계속 `DryRun`입니다. 이 프로필은 보호된 LocalAppData 설정만 읽고 상속된
-`MBN_STOCK_PLAYOUT_*`·DB 환경 변수와 기존 운영자 scene/resource/background 선택을 무시합니다.
-endpoint는 숫자형 loopback, scene root는 실행 파일 옆 검증된 `Cuts`로 다시 고정하며 strict
+`MBN_STOCK_PLAYOUT_*`·DB 환경 변수를 무시합니다. scene root는 첫 실행에서 저장한 원래
+`Cuts` 경로로 고정하며 strict
검증이 실패하면 Live를 승인하지 않고 안전한 DryRun으로 시작합니다.
같은 clone에서는 로컬 연결 파일이 Git에 덮어써지지 않으므로 이후에는 `git pull` 후 F5로
확인할 수 있습니다. 저장소를 새 폴더에 다시 clone하거나 자산·DB 폴더 연결이 바뀌면 첫 실행
-설정을 다시 진행합니다. K3D DLL이 최초 기준과 달라진 경우에는 첫 실행 설정을 반복해도 자동
+설정을 다시 진행합니다. K3D DLL이 최초 기준과 달라진 경우에는 경로 저장을 반복해도 자동
재승인되지 않습니다. 벤더 변경을 독립적으로 검증한 뒤 두 SHA-256을 명시해 수동 교체해야
합니다. pull 뒤 Required 자산 검증이 실패하는 경우에도 F5를 계속 시도하지 말고 설정 오류를
바로잡습니다.
diff --git a/docs/DEVELOPMENT_LIVE_HANDOFF.md b/docs/DEVELOPMENT_LIVE_HANDOFF.md
index 7058d36..0a7a51c 100644
--- a/docs/DEVELOPMENT_LIVE_HANDOFF.md
+++ b/docs/DEVELOPMENT_LIVE_HANDOFF.md
@@ -60,54 +60,23 @@ PowerShell 명령은 필요 없다.
3. 첫 번째 F5로 소스 전용 첫 실행 설정 앱을 연다. 이 프로세스는 DB 미연결과 DryRun을
강제하고 Tornado2/PGM에 연결하지 않는다.
4. `Cuts 폴더 선택`에서 실제 실행 자산의 `Cuts` 폴더를 선택한다.
-5. `Res 폴더 선택`에서 다음 파일들이 함께 있는 실제 설정 폴더를 선택한다.
- `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, `업종_코스닥.ini`, `해외.ini`,
- `환율.ini`, `지수.ini`, `종목비교.ini`.
+5. `Res 폴더 선택`에서 기존 코더가 사용하는 실제 `Res` 폴더를 선택한다.
6. 두 폴더가 같은 공통 부모 아래의 정확한 `Cuts`, `Res`인지 확인하고 `설정 시작`을 누른다.
7. `설정 완료`가 표시되면 창을 닫고 F5를 한 번 더 누른다.
-소스 트리의 오래된 `RES`, 백업 폴더, `MmoneyCoder - 복사본.ini`가 있는 다른 위치를
-선택하지 않는다. 화면 입력은 `Cuts`와 `Res` 두 폴더뿐이지만, 첫 실행 앱은
-`MmoneyCoder.ini`를 포함한 **활성 INI 8개**를 모두 확인한다. 7개 UI INI는 각각 파싱하고
-`MmoneyCoder.ini`는 DB 계약을 검증한다. 전체 Res를 무차별 복사하지 않고 폐쇄형 34개
-allowlist만 runtime으로 구성하며, 자격증명 INI는 별도 사용자 전용 경로로만 복사한다.
-개발 테스트 서버의 계정이라 노출을 허용하는 경우에도 자격증명을 Git이나 빌드 산출물에 넣지
-않는 계약은 그대로 유지한다.
+소스 트리의 오래된 `RES`나 이름이 비슷한 백업 폴더를 선택하지 않는다. `설정 시작`은
+두 폴더의 고정 로컬 경로만 다음 위치에 저장한다.
-초기화기는 다음 작업을 한 번에 수행해야 한다.
+- `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json`
+- 저장소의 Git 제외 `Directory.Build.local.props`
-- 선택한 `Cuts`와 `Res`의 고정 로컬 경로를
- `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json`에 저장한다.
- `Cuts`는 원래 폴더를 그대로 사용하며 전체 파일 열거, ZIP 생성, 복사, 크기·해시 검사를
- 하지 않는다. 실제 장면이나 영상이 사용될 때 그 파일만 개별 검증하므로 사용하지 않는
- 파일이 없거나 `Cuts\Video`가 일부 비어 있어도 초기화가 중단되지 않는다.
-- 두 폴더의 공통 부모를 명시적 MSBuild 속성으로 전달해, 저장소 binding을 아직 바꾸지 않은
- 상태에서 Debug x64 빌드를 먼저 검증한다. 빌드 출력에는 `Cuts` 복사본을 만들지 않는다.
-- 기존 DB INI를 `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini`로 복사하고 현재
- Windows 사용자 전용 ACL을 적용한다.
-- K3D x64 등록 상태를 읽기 전용으로 점검하고 검증된 설치본의 native/Interop SHA-256을
- 사용자 전용 `Config\k3d-pins.local.json`의 최초 기준으로 자동 고정한다. 이 파일이 이미
- 있으면 같은 값을 재사용하고, 등록 DLL이 달라졌을 때 자동 갱신하지 않는다.
-- Debug x64 빌드 후 선택한 runtime root가 직접 적용되었는지, SourceOnly가 아닌지,
- `Cuts` 복사본과 DB INI가 빌드 출력에 들어가지 않았는지 검증한다. `Res`는 UI에 필요한
- 폐쇄형 allowlist만 빌드 출력에 배치한다.
-- `127.0.0.1:30001` 개발 기본 endpoint의 보호된 로컬 Development Live 설정을 모든 선행
- 검증 뒤 새로 발급한다.
-- 저장소에서 제외되는 `Directory.Build.local.props`를 마지막 성공 표식으로 원자 기록해
- 선택한 두 폴더의 공통 부모를 `LegacyRuntimeAssetsMode=Required`로 사용하게 한다.
+이 버튼은 폴더 안의 파일을 열거·파싱·복사하지 않으며 PowerShell, `dotnet build`, DB 복사,
+K3D 검사, Live 승인 발급 또는 Tornado2/PGM 명령을 실행하지 않는다. 따라서 경로 저장은
+즉시 완료되어야 한다. `Cuts`와 `Res`의 실제 파일 검사는 다음 F5의 빌드 또는 해당 기능을
+사용하는 시점에 필요한 파일 단위로 수행된다.
-초기화 과정 자체는 Tornado2/PGM에 `CONNECT`하거나 PREPARE, TAKE IN, NEXT, TAKE OUT 등의
-명령을 보내지 않는다. 초기화 도중 오류가 나면 F5로 송출을 시도하지 말고 원인부터 바로잡는다.
-runtime 연결이나 DB overlay를 갱신한 뒤 선행 단계가 실패하면 기존 파일의 정확한 내용과
-ACL을 복구하고 Live 승인 파일은 제거된 상태로 둔다. 반면 K3D pin은 DLL 변경 뒤 재시도로
-자동 승인되는 일을 막는 지속 신뢰 기준이므로, 최초 생성 또는 독립 검증에 따른 명시적 교체가
-완료된 뒤 다른 설정 단계가 실패해도 그대로 보존한다.
-`Directory.Build.local.props`는 모든 기능 설정이 끝난 뒤에만 기록하므로, 설정 창이나
-Visual Studio가 그 전에 비정상 종료되면 다음 F5도 SourceOnly 첫 실행 화면으로 돌아와
-같은 두 폴더로 다시 진행할 수 있다.
-
-기본 초기화가 이미 빌드를 확인한다. 필요하면 같은 조건이 유지되는지 별도 MSBuild 인자나 환경
-변수 없이 다음 명령으로 다시 확인할 수 있다.
+전체 Debug x64 빌드는 설정 창을 닫고 누르는 다음 F5에서 진행된다. 필요하면 같은 조건을
+별도 MSBuild 인자나 환경 변수 없이 다음 명령으로 확인할 수 있다.
```powershell
dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj `
@@ -115,19 +84,23 @@ dotnet build .\src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyPar
-p:Platform=x64
```
-출력에 전체 runtime 자산이 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드,
-`bin\SourceOnly` 또는 `DryRun`만 표시되면 Development Live를 진행하지 않는다.
+출력에 외부 runtime 경로가 활성화되었다는 메시지가 있어야 한다. 소스 전용 모드가 계속
+표시되면 Visual Studio에서 다시 빌드한다. `DryRun`은 경로 저장 실패가 아니라 별도의
+Development Live 승인이 아직 없다는 뜻일 수 있다.
같은 clone에서는 이후 `git pull` 뒤 Visual Studio 2026에서 `Debug|x64`, 시작 대상
`Legacy Parity App (VS F5)`, 실행 프로필
`MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)`를 선택해 F5로 시작할 수
-있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 설정을 다시 진행한다. 자산·DB·K3D·PGM
-설정이 바뀌었거나 pull 뒤 Required 자산 검증이 실패하면 F5를 반복하지 말고 변경 원인을
-검토한다. 특히 K3D DLL 변경은 첫 실행 화면을 반복해도 자동 승인되지 않는다. 최초 실제 동작
+있다. 저장소를 새 폴더에 다시 clone하면 첫 실행 경로 저장을 다시 진행한다. DB overlay,
+K3D pin과 Development Live 승인이 없는 새 PC에서는 아래 수동 초기화 절차를 별도로
+완료하기 전까지 안전한 `DryRun`을 유지한다. K3D DLL 변경은 경로 저장을 반복해도 자동
+승인되지 않는다. 최초 실제 동작
확인은 아래 최소 인수 시퀀스의 `5001` 하나로 제한한다.
-### 화면 초기화를 사용할 수 없을 때
+### Development Live까지 준비할 때
-첫 실행 창을 열 수 없는 장애 대응에만 저장소 루트에서 다음 수동 등가 명령을 사용한다.
+첫 실행 경로 저장과 별개로 DB overlay, K3D pin과 Development Live 승인까지 새로
+준비해야 할 때 저장소 루트에서 다음 수동 명령을 사용한다. 이 명령은 전체 Debug 빌드
+검증까지 수행하므로 단순 폴더 저장 버튼보다 오래 걸리는 것이 정상이다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass `
@@ -143,9 +116,8 @@ powershell -NoProfile -ExecutionPolicy Bypass `
기존 로컬 runtime, DB 또는 Live 설정과 다르면 자동으로 덮어쓰지 않는다. 변경 대상을 검토한
뒤에만 `-ReplaceRuntimeBinding`, `-ReplaceDatabaseProfile`, `-ReplaceLiveConfig` 중 필요한
항목을 명시한다. 수동 모드는 기존처럼 독립 승인된 `-NativeSha256`과 `-InteropSha256`을
-사용할 수도 있으며, 이 두 값과 `-PinRegisteredK3D`는 함께 쓸 수 없다. 이 스크립트는 첫
-실행 화면이 내부에서 사용하는 초기화 도구의 수동 fallback이며 일반 clone 절차의 선행
-명령이 아니다.
+사용할 수도 있으며, 이 두 값과 `-PinRegisteredK3D`는 함께 쓸 수 없다. 이 스크립트는
+첫 실행 화면에서 자동으로 호출되지 않는다.
## 3. 자산이 없는 PC 대안: 검증된 Git 밖 runtime bundle 설치
@@ -337,8 +309,8 @@ runtime이나 DB 위치도 의도적으로 달라졌을 때만 각각 `-ReplaceR
## 6. 보호된 로컬 송출 설정
-2단계 첫 실행 설정은 지속 K3D pin을 확인한 뒤 아래 송출 설정 두 파일도 마지막 기능 설정
-단계에서 검증 후 새로 발급한다. 기존 승인은 매번 먼저 무효화하며 runtime·DB·Debug 빌드가 모두
+별도 수동 Development Live 초기화는 지속 K3D pin을 확인한 뒤 아래 송출 설정 두 파일도
+마지막 기능 설정 단계에서 검증 후 새로 발급한다. 기존 승인은 매번 먼저 무효화하며 runtime·DB·Debug 빌드가 모두
성공하지 않으면 새 Live 승인 파일을 남기지 않는다. 자산 없는 PC의 3단계 bundle 흐름처럼
설정만 별도로 초기화해야 할 때는 저장소 루트에서 다음 스크립트를 실행한다. 이 수동 경로의
두 해시는 별도로 확인한 값이어야 한다. 지정 개발 PC의 endpoint는
diff --git a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md
index 7b6ae62..4bbf3d9 100644
--- a/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md
+++ b/docs/DEVELOPMENT_LIVE_VISUAL_STUDIO.md
@@ -24,21 +24,21 @@
새 clone처럼 아직 검증된 runtime 연결이 없는 경우의 첫 F5는 예외적으로 소스 전용 설정
앱을 연다. 이 프로세스는 `--development-live`를 적용하지 않고 DB와 송출을 모두 차단한다.
-화면에서 기존 코더의 `Cuts`와 `Res` 폴더, 두 개만 선택한다. `Res` 안에서는
-`MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, `업종_코스닥.ini`, `해외.ini`,
-`환율.ini`, `지수.ini`, `종목비교.ini` 등 활성 INI 8개를 모두 확인한다. 개별 INI나
-Tornado2 endpoint를 입력하지 않으며 endpoint는 지정 개발 환경 기본값
-`127.0.0.1:30001`로 고정된다. 선택한 경로는 사용자 설정에 직접 저장하며 `Cuts` 전체를
-열거·복사·압축·해시 검사하지 않는다. 필요한 장면과 영상 파일은 실제 사용 시에만 개별
-확인하므로 일부 파일이 없어도 설정은 완료된다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더
-누른다. 두 번째 F5부터 위 세 조건을 검증해 Development Live를 적용한다. 자세한 절차는
+화면에서 기존 코더의 `Cuts`와 `Res` 폴더, 두 개만 선택한다. 설정 버튼은 폴더 안의 파일을
+열거·파싱·복사하지 않고 두 경로와 로컬 MSBuild binding만 저장한다. PowerShell,
+`dotnet build`, DB 복사, K3D 검사와 Live 승인 발급도 실행하지 않으므로 즉시 완료되어야
+한다. 설정을 완료한 뒤 창을 닫고 F5를 한 번 더 누르면 전체 앱 빌드가 시작된다.
+
+기존 유효한 Live 승인 파일이 없는 PC는 두 번째 F5에서도 안전한 `DryRun`을 유지한다.
+DB overlay, K3D pin과 Development Live 승인이 필요하면 아래 수동 초기화 절차를 별도로
+완료한다. 자세한 절차는
[개발 PGM 인수 절차](DEVELOPMENT_LIVE_HANDOFF.md#2-기존-자산-보유-pc-clone-후-1회-초기화)를
따른다.
`MmoneyCoder.ini`의 DB 자격증명은 테스트 개발 서버용이라도 Git에 넣지 않는다. 첫 실행
-설정은 사용자 전용 LocalAppData overlay로만 복사하며 빌드 출력, 선택 경로 설정과 MSIX에는
-포함하지 않는다. 첫 실행 화면이 열리지 않거나 기존 로컬 설정을 명시적으로 교체해야 할 때만
-`Initialize-ExistingDevelopmentPc.ps1`을 수동 fallback으로 사용한다.
+경로 저장은 이 파일을 복사하거나 읽지 않는다. 빌드 출력, 선택 경로 설정과 MSIX에도
+포함하지 않는다. 보호된 LocalAppData DB overlay와 Live 설정이 필요할 때만
+`Initialize-ExistingDevelopmentPc.ps1`을 수동으로 사용한다.
## Visual Studio 시작 대상과 프로필
@@ -73,7 +73,7 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴
### 지속 K3D pin
-첫 실행 설정은 K3D의 HKLM x64 등록, CLSID·ProgID·TypeLib, AMD64와 vendor 배치를 먼저
+수동 Development Live 초기화는 K3D의 HKLM x64 등록, CLSID·ProgID·TypeLib, AMD64와 vendor 배치를 먼저
검증한다. 검사를 통과한 최초 native/Interop DLL의 SHA-256은 다음 사용자 전용 파일에
지속 저장한다.
@@ -81,7 +81,7 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\k3d-pins.local.json
```
-이후 등록 DLL의 해시가 이 기준과 다르면 첫 실행 설정을 다시 눌러도 자동으로 pin을 바꾸지
+이후 등록 DLL의 해시가 이 기준과 다르면 폴더 경로를 다시 저장해도 자동으로 pin을 바꾸지
않고 Development Live를 차단한다. 정식 DLL 교체 시에는 벤더 배포본 등으로 독립 검증한
`-NativeSha256`과 `-InteropSha256`을 제공하고
`Initialize-ExistingDevelopmentPc.ps1 -ReplaceK3DPin -ReplaceLiveConfig` 수동 절차를
@@ -126,7 +126,7 @@ DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴
수동 생성·교체 시에는 현재 설치 파일의 해시를 그 자리에서 계산했다는 이유만으로 승인 값으로
사용해서는 안 된다. 벤더 배포본 또는 기존에 독립 검수된 증적과 대조한 값을 사용한다. 일반
-첫 실행에서는 위 지속 K3D pin을 먼저 만들거나 대조한 뒤 같은 기준값으로 이 승인 파일을
+수동 Development Live 초기화에서는 위 지속 K3D pin을 먼저 만들거나 대조한 뒤 같은 기준값으로 이 승인 파일을
발급한다. 파일 ACL은 현재 개발 사용자와 관리자만 수정할 수 있도록 유지한다.
## 기존 playout.local.json
diff --git a/docs/LEGACY_RUNTIME_ASSETS.md b/docs/LEGACY_RUNTIME_ASSETS.md
index 911fde4..d581168 100644
--- a/docs/LEGACY_RUNTIME_ASSETS.md
+++ b/docs/LEGACY_RUNTIME_ASSETS.md
@@ -26,9 +26,8 @@ Web\...
기본 `LegacyRuntimeAssetsMode`는 `Auto`다.
-- 초기화기가 선택한 두 경로와 빌드를 검증한 뒤 마지막 성공 표식으로 만든 Git 제외
- `Directory.Build.local.props`가 있으면, 그 공통 부모 아래의 원래 `Cuts`와 `Res` 경로를
- 직접 사용한다.
+- 첫 실행 창이 선택한 두 경로를 저장하고 만든 Git 제외 `Directory.Build.local.props`가
+ 있으면, 그 공통 부모 아래의 원래 `Cuts`와 `Res` 경로를 직접 사용한다.
- 새 Git clone의 Debug/F5는 주변의 과거 원본 경로를 자동 선택하지 않고 소스 전용 첫 실행
설정 앱을 만든다.
- 원본 경로를 명시하거나 Release MSIX를 만들면 기본값이 `Required`로 바뀌어 누락 자산을
@@ -38,15 +37,13 @@ Web\...
표시한다. 이 프로세스는 DB를 초기화하지 않고 과거 로컬 송출 프로필, 운영자 송출·배경 경로,
프로세스 환경 override와 Development Live 실행 인수를 사용하지 않으며 안전한 DryRun을
강제한다. 설정이 성공하면 창을 닫고 Visual Studio에서 F5를 한 번 더 눌러 전체 자산
-Development Live 빌드를 시작한다. 출력과 중간 패키징 파일은 이전 전체 빌드의 자산이나
+빌드를 시작한다. 출력과 중간 패키징 파일은 이전 전체 빌드의 자산이나
`MmoneyCoder.ini`를 재사용하지 않도록 다음 별도 경로에 생성된다.
-첫 실행 화면에서 개별 INI, DB endpoint 또는 Tornado2 port를 따로 입력하지 않는다. 선택한
-`Res`에서 `MmoneyCoder.ini`, `종목.ini`, `업종_코스피.ini`, `업종_코스닥.ini`,
-`해외.ini`, `환율.ini`, `지수.ini`, `종목비교.ini` 등 활성 INI 8개를 모두 검증하고,
-Tornado2 endpoint는 지정 개발 환경의 `127.0.0.1:30001`로 고정한다. 화면을 사용할 수 없는
-장애 대응 또는 기존 로컬 설정의 명시적 교체에만 `Initialize-ExistingDevelopmentPc.ps1`을
-수동 fallback으로 사용한다.
+첫 실행 화면은 선택한 두 폴더의 고정 로컬 경로와 `Cuts`/`Res` 이름·공통 부모만 확인한다.
+폴더 내부 파일을 열거·파싱·복사하지 않고 PowerShell, 빌드, DB, K3D 또는 Live 설정도
+실행하지 않는다. DB overlay와 Development Live 준비가 필요한 경우에만
+`Initialize-ExistingDevelopmentPc.ps1`을 별도 수동 절차로 사용한다.
```text
src\MBN_STOCK_WEBVIEW.LegacyParityApp\bin\SourceOnly\...
@@ -90,7 +87,7 @@ Git에는 `Cuts`, DB 자격증명, 벤더 DLL, 라이선스, 인증서 또는
좌측 메뉴 하단의 `설정`에서는 네이티브 폴더 선택 창으로 디자인(`Cuts`) 폴더, 설정(`Res`)
폴더와 선택 사항인 운영 배경 폴더를 직접 지정할 수 있다. 선택한 고정 로컬 경로와 `Res`
-카탈로그 형식을 확인한 뒤 다음 앱 전용 파일에 저장한다. `Cuts` 내용 전체는 검사하지 않는다.
+폴더 자체만 확인한 뒤 다음 앱 전용 파일에 저장한다. 폴더 내용 전체는 검사하지 않는다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\runtime-folders.local.json
@@ -100,7 +97,7 @@ Git에는 `Cuts`, DB 자격증명, 벤더 DLL, 라이선스, 인증서 또는
적용된다. 같은 화면의 `왼쪽 메뉴 펼쳐서 시작`은 즉시 화면에 반영되고 다음 시작 상태도 위 파일에
저장된다. 절대 경로는 Web 콘텐츠에 전달하지 않는다.
-이 화면은 자산 위치, 메뉴 시작 상태와 선택한 `Res` 안의 검증된 DB 프로필 위치만 관리한다.
+이 화면은 자산 위치와 메뉴 시작 상태만 관리한다.
DB 자격증명의 내용은 표시하거나 직접 편집하지 않는다. Release/default `DryRun`, KTAP 호스트·
포트·채널, Test/Live 게이트, scene allowlist와 벤더 해시는 보호 설정으로 유지되며 폴더를 선택해도
완화되거나 변경되지 않는다.
@@ -119,13 +116,13 @@ CP949 레거시 형식으로 읽고, 파일의 section/row/order를 기존의
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Res\MmoneyCoder.ini
```
-이 규칙은 endpoint가 노출되어도 무방한 테스트 개발 서버인 경우에도 같다. 첫 실행 설정은
-원본 `MmoneyCoder.ini`를 위 사용자 전용 경로에만 복사하고 현재 Windows 사용자 전용 ACL로
-보호한다. DB 자격증명은 Git, Debug/Release 출력, 게시, runtime bundle과 MSIX에 포함하지
-않는다.
+이 규칙은 endpoint가 노출되어도 무방한 테스트 개발 서버인 경우에도 같다. 첫 실행 폴더
+저장은 원본 `MmoneyCoder.ini`를 읽거나 복사하지 않는다. 수동 Development Live 초기화에서
+필요할 때만 위 사용자 전용 경로에 복사하고 현재 Windows 사용자 전용 ACL로 보호한다.
+DB 자격증명은 Git, Debug/Release 출력, 게시, runtime bundle과 MSIX에 포함하지 않는다.
-사용자 설정 메뉴에서 별도 `Res` 폴더를 선택했고 그 안에 `MmoneyCoder.ini`가 있으면 앱은 저장
-전에 형식을 네이티브에서 검증하고 다음 시작부터 그 DB 설정을 우선 사용한다. 파일 내용과
+사용자 설정 메뉴에서 별도 `Res` 폴더를 선택하면 앱은 경로만 저장한다. 일반 Debug/DryRun은
+다음 시작부터 그 폴더의 DB 설정을 사용할 수 있지만 형식 검사는 실제 로드 시 수행한다. 파일 내용과
자격증명은 Web 화면에 노출하지 않으며 화면에서 직접 편집할 수도 없다. 단, one-shot Gate A
검증 실행에서는 이 사용자 선택을 무시하고 검증 계획에 고정된 `database.local.json`만 사용한다.
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs b/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs
index 4a9eac0..4261c45 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs
+++ b/src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorSettings.cs
@@ -93,20 +93,6 @@ public static class LegacyOperatorFolderValidator
private const string InvalidFolderMessage =
"선택한 폴더를 안전하게 사용할 수 없습니다.";
- private const string InvalidResourceMessage =
- "선택한 설정 폴더의 UI 설정 파일을 검증할 수 없습니다.";
-
- private static readonly string[] ResourceFileNames =
- [
- "종목.ini",
- "업종_코스피.ini",
- "업종_코스닥.ini",
- "해외.ini",
- "환율.ini",
- "지수.ini",
- "종목비교.ini"
- ];
-
private static readonly IReadOnlyList RequiredSceneFileNameValues =
Array.AsReadOnly(LegacySceneRuntimeCoverage.ExpectedCutCodes
.Select(code => string.Concat(code, ".t2s"))
@@ -184,54 +170,11 @@ public static class LegacyOperatorFolderValidator
switch (kind)
{
case LegacyOperatorSettingsFolderKind.Scene:
- // Cuts is an operator-owned external runtime directory. Individual
- // scenes and media are optional and are validated only when a
- // command actually resolves that file.
- break;
-
case LegacyOperatorSettingsFolderKind.Resource:
- foreach (var fileName in ResourceFileNames)
- {
- if (!TryValidateRegularFile(
- Path.Combine(canonicalPath, fileName),
- required: true,
- out failure))
- {
- return Invalid(failure, InvalidResourceMessage);
- }
- }
-
- var stockMenu = LegacyRuntimeStockCutMenuLoader
- .ResolveFromResourceDirectory(canonicalPath);
- if (stockMenu.UsesBuiltInCatalog ||
- stockMenu.Failure != LegacyRuntimeStockCutMenuFailure.None)
- {
- return Invalid(
- LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog,
- InvalidResourceMessage);
- }
-
- var uiCatalogs = LegacyRuntimeUiCatalogLoader
- .ResolveFromResourceDirectory(canonicalPath);
- if (uiCatalogs.Warnings.Count != 0)
- {
- return Invalid(
- LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog,
- InvalidResourceMessage);
- }
-
- var databaseIniPath = Path.Combine(canonicalPath, "MmoneyCoder.ini");
- if (!TryInspectOptionalRegularFile(
- databaseIniPath,
- out databaseIniDetected,
- out failure))
- {
- return Invalid(failure, InvalidResourceMessage);
- }
-
- break;
-
case LegacyOperatorSettingsFolderKind.Background:
+ // Folder selection stores only the canonical external path.
+ // Individual Cuts/Res files are resolved when the feature that
+ // needs them is used.
break;
default:
@@ -385,168 +328,6 @@ public static class LegacyOperatorFolderValidator
}
}
- private static bool TryValidateRegularFile(
- string path,
- bool required,
- out LegacyOperatorFolderValidationFailure failure)
- {
- return TryValidateRegularFile(path, required, out _, out failure);
- }
-
- private static bool TryValidateRegularFile(
- string path,
- bool required,
- out bool detected,
- out LegacyOperatorFolderValidationFailure failure)
- {
- detected = false;
- try
- {
- var attributes = File.GetAttributes(path);
- if ((attributes & (FileAttributes.Directory |
- FileAttributes.ReparsePoint |
- FileAttributes.Device)) != 0)
- {
- failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe;
- return false;
- }
-
- if (new FileInfo(path).Length <= 0)
- {
- failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe;
- return false;
- }
-
- detected = true;
- failure = LegacyOperatorFolderValidationFailure.None;
- return true;
- }
- catch (Exception exception) when (
- exception is FileNotFoundException or DirectoryNotFoundException)
- {
- failure = required
- ? LegacyOperatorFolderValidationFailure.RequiredFileMissing
- : LegacyOperatorFolderValidationFailure.None;
- return !required;
- }
- catch (Exception exception) when (
- exception is ArgumentException or IOException or UnauthorizedAccessException or
- NotSupportedException or System.Security.SecurityException)
- {
- failure = LegacyOperatorFolderValidationFailure.FileUnavailable;
- return false;
- }
- }
-
- private static bool TryValidateContainedRegularFile(
- string root,
- string relativePath,
- bool required,
- out bool detected,
- out LegacyOperatorFolderValidationFailure failure)
- {
- detected = false;
- try
- {
- if (string.IsNullOrWhiteSpace(relativePath) ||
- Path.IsPathFullyQualified(relativePath) ||
- relativePath.Any(char.IsControl))
- {
- failure = LegacyOperatorFolderValidationFailure.InvalidPath;
- return false;
- }
-
- var fullPath = Path.GetFullPath(Path.Combine(root, relativePath));
- var rootPrefix = string.Concat(
- Path.TrimEndingDirectorySeparator(root),
- Path.DirectorySeparatorChar);
- if (!fullPath.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase))
- {
- failure = LegacyOperatorFolderValidationFailure.InvalidPath;
- return false;
- }
-
- var relativeDirectory = Path.GetDirectoryName(relativePath);
- if (!string.IsNullOrEmpty(relativeDirectory))
- {
- var current = root;
- foreach (var segment in relativeDirectory.Split(
- [Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar],
- StringSplitOptions.RemoveEmptyEntries))
- {
- current = Path.Combine(current, segment);
- try
- {
- var attributes = File.GetAttributes(current);
- if ((attributes & FileAttributes.Directory) == 0 ||
- (attributes & (FileAttributes.ReparsePoint |
- FileAttributes.Device)) != 0)
- {
- failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe;
- return false;
- }
- }
- catch (Exception exception) when (
- exception is FileNotFoundException or DirectoryNotFoundException)
- {
- break;
- }
- }
- }
-
- return TryValidateRegularFile(fullPath, required, out detected, out failure);
- }
- catch (Exception exception) when (
- exception is ArgumentException or IOException or UnauthorizedAccessException or
- NotSupportedException or System.Security.SecurityException)
- {
- failure = LegacyOperatorFolderValidationFailure.FileUnavailable;
- return false;
- }
- }
-
- private static bool TryInspectOptionalRegularFile(
- string path,
- out bool detected,
- out LegacyOperatorFolderValidationFailure failure)
- {
- detected = false;
- try
- {
- var attributes = File.GetAttributes(path);
- if ((attributes & (FileAttributes.Directory |
- FileAttributes.ReparsePoint |
- FileAttributes.Device)) != 0)
- {
- failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe;
- return false;
- }
-
- if (new FileInfo(path).Length <= 0)
- {
- failure = LegacyOperatorFolderValidationFailure.RequiredFileUnsafe;
- return false;
- }
-
- detected = true;
- failure = LegacyOperatorFolderValidationFailure.None;
- return true;
- }
- catch (Exception exception) when (
- exception is FileNotFoundException or DirectoryNotFoundException)
- {
- failure = LegacyOperatorFolderValidationFailure.None;
- return true;
- }
- catch (Exception exception) when (
- exception is ArgumentException or IOException or UnauthorizedAccessException or
- NotSupportedException or System.Security.SecurityException)
- {
- failure = LegacyOperatorFolderValidationFailure.FileUnavailable;
- return false;
- }
- }
-
private static LegacyOperatorFolderValidationResult Invalid(
LegacyOperatorFolderValidationFailure failure,
string warningMessage) => new(
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml
index f56499f..a6d926e 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml
@@ -23,7 +23,7 @@
@@ -66,7 +66,7 @@
Text="2. 설정/INI 폴더 (Res)" />
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs
index 85da2da..4d16f65 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/FirstRunSetupWindow.xaml.cs
@@ -1,7 +1,6 @@
-using System.Diagnostics;
using System.Reflection;
using System.Text;
-using MBN_STOCK_WEBVIEW.Infrastructure;
+using System.Xml;
using MBN_STOCK_WEBVIEW.LegacyApplication;
using Microsoft.UI.Windowing;
using Windows.Graphics;
@@ -13,11 +12,8 @@ namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
public sealed partial class FirstRunSetupWindow : Window
{
private const string RepositoryRootMetadataKey = "SourceRepositoryRoot";
- private const string SetupScriptRelativePath =
- @"scripts\Initialize-ExistingDevelopmentPc.ps1";
- private const string DefaultPlayoutHost = "127.0.0.1";
- private const int DefaultPlayoutPort = 30001;
- private const int MaximumDiagnosticCharacters = 32 * 1024;
+ private const string LocalBuildPropsFileName =
+ "Directory.Build.local.props";
private string? _cutsDirectory;
private string? _resourceDirectory;
@@ -81,20 +77,12 @@ public sealed partial class FirstRunSetupWindow : Window
return;
}
- if (!validation.Folder.DatabaseIniDetected ||
- !CanReadDatabaseIni(validation.Folder.CanonicalPath))
- {
- ShowWarning(
- "선택한 Res 폴더의 MmoneyCoder.ini를 검증할 수 없습니다.");
- return;
- }
-
_resourceDirectory = validation.Folder.CanonicalPath;
ResourcePathText.Text = _resourceDirectory;
RefreshReadyState();
}
- private async void OnInstallClicked(object sender, RoutedEventArgs e)
+ private void OnInstallClicked(object sender, RoutedEventArgs e)
{
if (_setupRunning)
{
@@ -124,52 +112,22 @@ public sealed partial class FirstRunSetupWindow : Window
}
SetSetupRunning(true);
- SetupStatusBar.Title = "설정 중";
+ SetupStatusBar.Title = "경로 저장 중";
SetupStatusBar.Message =
- "선택한 경로와 DB 설정·K3D를 적용하고 있습니다. 완료될 때까지 Visual Studio 실행을 중지하지 마세요.";
+ "선택한 Cuts/Res 폴더 경로를 저장하고 있습니다.";
SetupStatusBar.Severity = InfoBarSeverity.Informational;
try
{
- var result = await RunSetupAsync(
+ SaveSelectedRuntimePaths(
repositoryRoot,
runtimeRoot!,
- _resourceDirectory!,
- replaceExistingLocalConfiguration: false);
- if (!result.Succeeded &&
- result.LocalConfigurationReplacementRequired)
- {
- var replaceConfirmed =
- await ConfirmLocalConfigurationReplacementAsync();
- if (!replaceConfirmed)
- {
- ShowWarning(
- "기존 로컬 설정은 변경하지 않았습니다. 다시 진행하려면 설정 시작을 눌러 주세요.");
- return;
- }
-
- SetupStatusBar.Title = "기존 설정 업데이트 중";
- SetupStatusBar.Message =
- "선택한 Cuts/Res와 기본 Tornado2 설정으로 이 앱의 로컬 설정을 갱신하고 있습니다.";
- result = await RunSetupAsync(
- repositoryRoot,
- runtimeRoot!,
- _resourceDirectory!,
- replaceExistingLocalConfiguration: true);
- }
-
- if (!result.Succeeded)
- {
- ShowError(
- string.IsNullOrWhiteSpace(result.Diagnostic)
- ? "첫 실행 설정에 실패했습니다. 선택한 폴더와 K3D 설치 상태를 확인해 주세요."
- : $"첫 실행 설정에 실패했습니다. {result.Diagnostic}");
- return;
- }
+ _cutsDirectory!,
+ _resourceDirectory!);
SetupStatusBar.Title = "설정 완료";
SetupStatusBar.Message =
- "이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요. Development Live로 실행됩니다.";
+ "폴더 경로를 저장했습니다. 이 창을 닫고 Visual Studio에서 F5를 한 번 더 누르세요.";
SetupStatusBar.Severity = InfoBarSeverity.Success;
InstallButton.Visibility = Visibility.Collapsed;
CloseButton.Visibility = Visibility.Visible;
@@ -177,7 +135,7 @@ public sealed partial class FirstRunSetupWindow : Window
catch
{
ShowError(
- "첫 실행 설정을 시작하지 못했습니다. Visual Studio와 Windows PowerShell 설치 상태를 확인해 주세요.");
+ "폴더 경로를 저장하지 못했습니다. 폴더와 Git 소스 위치를 확인해 주세요.");
}
finally
{
@@ -313,20 +271,6 @@ public sealed partial class FirstRunSetupWindow : Window
return true;
}
- private static bool CanReadDatabaseIni(string resourceDirectory)
- {
- try
- {
- _ = new LegacyIniDatabaseOptionsLoader(_ => null).Load(
- Path.Combine(resourceDirectory, "MmoneyCoder.ini"));
- return true;
- }
- catch
- {
- return false;
- }
- }
-
private static string ResolveRepositoryRoot()
{
var metadata = typeof(FirstRunSetupWindow).Assembly
@@ -343,15 +287,12 @@ public sealed partial class FirstRunSetupWindow : Window
var repositoryRoot = Path.TrimEndingDirectorySeparator(
Path.GetFullPath(metadata.Value));
- var setupScript = Path.Combine(
- repositoryRoot,
- SetupScriptRelativePath);
var project = Path.Combine(
repositoryRoot,
"src",
"MBN_STOCK_WEBVIEW.LegacyParityApp",
"MBN_STOCK_WEBVIEW.LegacyParityApp.csproj");
- if (!File.Exists(setupScript) || !File.Exists(project))
+ if (!File.Exists(project))
{
throw new InvalidOperationException(
"The source repository metadata is not usable.");
@@ -360,161 +301,119 @@ public sealed partial class FirstRunSetupWindow : Window
return repositoryRoot;
}
- private static async Task RunSetupAsync(
+ private static void SaveSelectedRuntimePaths(
string repositoryRoot,
string runtimeRoot,
- string resourceDirectory,
- bool replaceExistingLocalConfiguration)
+ string cutsDirectory,
+ string resourceDirectory)
{
- var setupScript = Path.Combine(
- repositoryRoot,
- SetupScriptRelativePath);
- var startInfo = new ProcessStartInfo
+ var store = new LegacyOperatorSettingsStore();
+ var loaded = store.Load();
+ var current = loaded.Failure == LegacyOperatorSettingsStoreFailure.None
+ ? loaded.Settings
+ : LegacyOperatorSettings.Default;
+ var saved = store.Save(current with
{
- FileName = ResolveWindowsPowerShellPath(),
- WorkingDirectory = repositoryRoot,
- UseShellExecute = false,
- CreateNoWindow = true,
- RedirectStandardOutput = true,
- RedirectStandardError = true
- };
- foreach (var argument in new[]
- {
- "-NoLogo",
- "-NoProfile",
- "-NonInteractive",
- "-ExecutionPolicy",
- "Bypass",
- "-File",
- setupScript,
- "-LegacyRuntimeSourceRoot",
- runtimeRoot,
- "-NoFolderPicker",
- "-DatabaseIniPath",
- Path.Combine(resourceDirectory, "MmoneyCoder.ini"),
- "-ConfigureDevelopmentLive",
- "-PlayoutHost",
- DefaultPlayoutHost,
- "-PlayoutPort",
- DefaultPlayoutPort.ToString(
- System.Globalization.CultureInfo.InvariantCulture),
- "-PinRegisteredK3D"
- })
- {
- startInfo.ArgumentList.Add(argument);
- }
- if (replaceExistingLocalConfiguration)
- {
- startInfo.ArgumentList.Add("-ReplaceRuntimeBinding");
- startInfo.ArgumentList.Add("-ReplaceDatabaseProfile");
- startInfo.ArgumentList.Add("-ReplaceLiveConfig");
- }
-
- using var process = Process.Start(startInfo) ??
- throw new InvalidOperationException("Windows PowerShell did not start.");
- var standardOutput = ReadBoundedTailAsync(process.StandardOutput);
- var standardError = ReadBoundedTailAsync(process.StandardError);
- await process.WaitForExitAsync();
- await Task.WhenAll(standardOutput, standardError);
-
- var diagnosticText =
- string.IsNullOrWhiteSpace(standardError.Result)
- ? standardOutput.Result
- : standardError.Result;
- return new FirstRunSetupResult(
- process.ExitCode == 0,
- LastDiagnosticLine(diagnosticText),
- RequiresLocalConfigurationReplacement(diagnosticText));
- }
-
- private async Task ConfirmLocalConfigurationReplacementAsync()
- {
- var dialog = new ContentDialog
- {
- XamlRoot = Content.XamlRoot,
- Title = "기존 로컬 설정 업데이트",
- Content =
- "이 PC에 이 앱의 이전 runtime, DB 또는 Development Live 설정이 있습니다. " +
- "선택한 Cuts/Res와 기본 Tornado2(127.0.0.1:30001) 설정으로 갱신할까요? " +
- "K3D DLL 기준값은 자동으로 바꾸지 않습니다.",
- PrimaryButtonText = "업데이트",
- CloseButtonText = "취소",
- DefaultButton = ContentDialogButton.Primary
- };
- return await dialog.ShowAsync() == ContentDialogResult.Primary;
- }
-
- private static bool RequiresLocalConfigurationReplacement(
- string? diagnostic) =>
- !string.IsNullOrWhiteSpace(diagnostic) &&
- (diagnostic.Contains(
- "-ReplaceRuntimeBinding",
- StringComparison.Ordinal) ||
- diagnostic.Contains(
- "-ReplaceDatabaseProfile",
- StringComparison.Ordinal) ||
- diagnostic.Contains(
- "-ReplaceLiveConfig",
- StringComparison.Ordinal));
-
- private static string ResolveWindowsPowerShellPath()
- {
- var systemDirectory = Environment.GetFolderPath(
- Environment.SpecialFolder.System);
- if (string.IsNullOrWhiteSpace(systemDirectory))
+ SceneDirectory = cutsDirectory,
+ ResourceDirectory = resourceDirectory
+ });
+ if (!saved.Succeeded)
{
throw new InvalidOperationException(
- "The Windows system directory is unavailable.");
+ "The selected runtime folders could not be saved.");
}
- var path = Path.GetFullPath(Path.Combine(
- systemDirectory,
- "WindowsPowerShell",
- "v1.0",
- "powershell.exe"));
- if (!File.Exists(path))
- {
- throw new FileNotFoundException(
- "Windows PowerShell 5.1 was not found.",
- path);
- }
-
- return path;
+ SaveRuntimeBinding(repositoryRoot, runtimeRoot);
}
- private static async Task ReadBoundedTailAsync(StreamReader reader)
+ private static void SaveRuntimeBinding(
+ string repositoryRoot,
+ string runtimeRoot)
{
- var tail = new StringBuilder(MaximumDiagnosticCharacters);
- var buffer = new char[4096];
- int count;
- while ((count = await reader.ReadAsync(buffer)) > 0)
+ var destination = Path.Combine(
+ repositoryRoot,
+ LocalBuildPropsFileName);
+ if (Directory.Exists(destination))
{
- tail.Append(buffer, 0, count);
- if (tail.Length > MaximumDiagnosticCharacters)
+ throw new InvalidOperationException(
+ "The local runtime binding path is not a file.");
+ }
+
+ if (File.Exists(destination))
+ {
+ var attributes = File.GetAttributes(destination);
+ if ((attributes & (FileAttributes.Directory |
+ FileAttributes.ReparsePoint |
+ FileAttributes.Device)) != 0)
{
- tail.Remove(
- 0,
- tail.Length - MaximumDiagnosticCharacters);
+ throw new InvalidOperationException(
+ "The local runtime binding file is unsafe.");
}
}
- return tail.ToString();
+ var bytes = CreateRuntimeBinding(runtimeRoot);
+ var temporary = Path.Combine(
+ repositoryRoot,
+ $".{LocalBuildPropsFileName}.{Guid.NewGuid():N}.tmp");
+ try
+ {
+ using (var stream = new FileStream(
+ temporary,
+ FileMode.CreateNew,
+ FileAccess.Write,
+ FileShare.None,
+ bufferSize: 4096,
+ FileOptions.WriteThrough))
+ {
+ stream.Write(bytes);
+ stream.Flush(flushToDisk: true);
+ }
+
+ File.Move(temporary, destination, overwrite: true);
+ temporary = string.Empty;
+ if (!File.ReadAllBytes(destination).AsSpan().SequenceEqual(bytes))
+ {
+ throw new IOException(
+ "The local runtime binding failed verification.");
+ }
+ }
+ finally
+ {
+ if (!string.IsNullOrEmpty(temporary) &&
+ File.Exists(temporary))
+ {
+ File.Delete(temporary);
+ }
+ }
}
- private static string? LastDiagnosticLine(string diagnostic)
+ private static byte[] CreateRuntimeBinding(string runtimeRoot)
{
- var line = diagnostic
- .Split(
- ['\r', '\n'],
- StringSplitOptions.RemoveEmptyEntries |
- StringSplitOptions.TrimEntries)
- .LastOrDefault();
- if (string.IsNullOrWhiteSpace(line))
+ using var stream = new MemoryStream(capacity: 512);
+ var settings = new XmlWriterSettings
{
- return null;
+ Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false),
+ Indent = true,
+ NewLineChars = Environment.NewLine,
+ NewLineHandling = NewLineHandling.Replace
+ };
+ using (var writer = XmlWriter.Create(stream, settings))
+ {
+ writer.WriteStartDocument();
+ writer.WriteStartElement("Project");
+ writer.WriteStartElement("PropertyGroup");
+ writer.WriteElementString(
+ "LegacyRuntimeSourceRoot",
+ runtimeRoot);
+ writer.WriteElementString(
+ "LegacyRuntimeAssetsMode",
+ "Required");
+ writer.WriteEndElement();
+ writer.WriteEndElement();
+ writer.WriteEndDocument();
}
- return line.Length <= 400 ? line : string.Concat(line[..397], "...");
+ return stream.ToArray();
}
private void SetSetupRunning(bool running)
@@ -551,8 +450,4 @@ public sealed partial class FirstRunSetupWindow : Window
return AppWindow.GetFromWindowId(windowId);
}
- private sealed record FirstRunSetupResult(
- bool Succeeded,
- string? Diagnostic,
- bool LocalConfigurationReplacementRequired);
}
diff --git a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
index ee5a172..df8ea79 100644
--- a/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
+++ b/src/MBN_STOCK_WEBVIEW.LegacyParityApp/MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
@@ -26,8 +26,8 @@
0.1.0
$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..'))