fix: persist first-run folder paths only
This commit is contained in:
@@ -11,9 +11,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Machine-local development runtime roots belong outside Git. The handoff
|
||||
initializer writes this optional file after verifying the separately
|
||||
delivered Cuts/Res bundle and its SHA-256 manifest.
|
||||
Machine-local development runtime roots belong outside Git. The first-run
|
||||
folder picker writes this optional direct path binding; the manual handoff
|
||||
initializer may also replace it for a separately delivered runtime.
|
||||
-->
|
||||
<Import Project="$(MSBuildThisFileDirectory)Directory.Build.local.props"
|
||||
Condition="Exists('$(MSBuildThisFileDirectory)Directory.Build.local.props')" />
|
||||
|
||||
55
README.md
55
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를 계속 시도하지 말고 설정 오류를
|
||||
바로잡습니다.
|
||||
|
||||
@@ -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는
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`만 사용한다.
|
||||
|
||||
|
||||
@@ -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<string> 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(
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<TextBlock
|
||||
FontSize="15"
|
||||
Foreground="#FFBFD2E6"
|
||||
Text="기존 코더의 Cuts 폴더와 여러 INI가 들어 있는 Res 폴더만 선택하면 됩니다."
|
||||
Text="기존 코더의 Cuts 폴더와 Res 폴더를 선택하면 해당 경로만 저장합니다."
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
Text="2. 설정/INI 폴더 (Res)" />
|
||||
<TextBlock
|
||||
Foreground="#FF8FAAC5"
|
||||
Text="MmoneyCoder.ini와 종목·업종·해외·환율·지수·종목비교 INI를 함께 확인합니다."
|
||||
Text="폴더 안의 파일 전체를 검사하거나 복사하지 않고 선택한 경로를 그대로 사용합니다."
|
||||
TextWrapping="Wrap" />
|
||||
<TextBlock
|
||||
x:Name="ResourcePathText"
|
||||
@@ -115,7 +115,7 @@
|
||||
<TextBlock
|
||||
Foreground="#FF8FAAC5"
|
||||
FontSize="13"
|
||||
Text="설정 중에는 DB나 Tornado2에 연결하지 않습니다. 완료 후 Visual Studio에서 F5를 한 번 더 누르면 실제 개발 송출 모드로 시작됩니다."
|
||||
Text="설정 시작은 경로만 저장합니다. 빌드, DB, K3D 또는 Tornado2 작업은 실행하지 않으며 완료 후 Visual Studio에서 F5를 한 번 더 누르면 앱을 빌드해 시작합니다."
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
@@ -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<FirstRunSetupResult> 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<bool> 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<string> 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);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<Version>0.1.0</Version>
|
||||
<!--
|
||||
Source-only F5 runs from an MSIX layout, not necessarily from the clone.
|
||||
Embed this local, non-secret build path so the first-run window can invoke
|
||||
the reviewed setup scripts in the exact clone that Visual Studio built.
|
||||
Embed this local, non-secret build path so the first-run window can write
|
||||
the direct local runtime binding into the exact clone Visual Studio built.
|
||||
-->
|
||||
<SourceRepositoryRoot>$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)\..\..'))</SourceRepositoryRoot>
|
||||
<!--
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using MBN_STOCK_WEBVIEW.Infrastructure;
|
||||
using MBN_STOCK_WEBVIEW.LegacyApplication;
|
||||
using MBN_STOCK_WEBVIEW.LegacyBridge;
|
||||
using MMoneyCoderSharp.Data;
|
||||
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyParityApp;
|
||||
|
||||
@@ -107,16 +106,6 @@ public sealed partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind == LegacyOperatorFolderKind.Resource &&
|
||||
validated.DatabaseIniDetected &&
|
||||
!CanReadSelectedDatabaseProfile(validated.CanonicalPath))
|
||||
{
|
||||
SetOperatorSettingsMessage(
|
||||
"선택한 설정 폴더의 DB 설정 파일을 검증할 수 없습니다.",
|
||||
LegacyOperatorSettingsMessageKind.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var next = kind switch
|
||||
{
|
||||
LegacyOperatorFolderKind.Design => _operatorSettings with
|
||||
@@ -270,20 +259,6 @@ public sealed partial class MainWindow
|
||||
LegacyOperatorSettingsMessageKind.Success);
|
||||
}
|
||||
|
||||
private static bool CanReadSelectedDatabaseProfile(string resourceDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
_ = new LegacyIniDatabaseOptionsLoader(_ => null).Load(
|
||||
Path.Combine(resourceDirectory, "MmoneyCoder.ini"));
|
||||
return true;
|
||||
}
|
||||
catch (DatabaseConfigurationException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetOperatorSettingsMessage(
|
||||
string message,
|
||||
LegacyOperatorSettingsMessageKind kind)
|
||||
|
||||
@@ -138,39 +138,26 @@ public sealed class LegacyOperatorSettingsTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResourceValidation_ParsesEveryClosedUiIniAndOnlyFlagsDatabaseIni()
|
||||
public void ResourceValidation_AcceptsAnEmptyResDirectoryAsADirectRuntimePath()
|
||||
{
|
||||
using var directory = new TemporaryDirectory();
|
||||
var resourceDirectory = directory.CreateDirectory("Res");
|
||||
WriteValidResourceFiles(resourceDirectory);
|
||||
File.WriteAllText(
|
||||
Path.Combine(resourceDirectory, "MmoneyCoder.ini"),
|
||||
"secret-content-that-must-not-be-returned");
|
||||
|
||||
var stockMenu = LegacyRuntimeStockCutMenuLoader
|
||||
.ResolveFromResourceDirectory(resourceDirectory);
|
||||
Assert.False(stockMenu.UsesBuiltInCatalog);
|
||||
var uiCatalogs = LegacyRuntimeUiCatalogLoader
|
||||
.ResolveFromResourceDirectory(resourceDirectory);
|
||||
Assert.Empty(uiCatalogs.Warnings);
|
||||
|
||||
var result = LegacyOperatorFolderValidator.Validate(
|
||||
LegacyOperatorSettingsFolderKind.Resource,
|
||||
resourceDirectory);
|
||||
|
||||
Assert.True(result.IsValid, $"{result.Failure}: {result.WarningMessage}");
|
||||
Assert.True(result.Folder!.DatabaseIniDetected);
|
||||
Assert.DoesNotContain("secret", result.Folder.AbbreviatedPath,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
Assert.Equal(Path.GetFullPath(resourceDirectory), result.Folder!.CanonicalPath);
|
||||
Assert.False(result.Folder.DatabaseIniDetected);
|
||||
Assert.Null(result.WarningMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResourceValidation_InvalidClosedIniFailsWithoutPartialAcceptance()
|
||||
public void ResourceValidation_DoesNotParseIniContents()
|
||||
{
|
||||
using var directory = new TemporaryDirectory();
|
||||
var resourceDirectory = directory.CreateDirectory("private-runtime-resources");
|
||||
WriteValidResourceFiles(resourceDirectory);
|
||||
WriteCp949(
|
||||
Path.Combine(resourceDirectory, "종목.ini"),
|
||||
["[종목]", "등록되지_않은_컷"]);
|
||||
@@ -179,32 +166,26 @@ public sealed class LegacyOperatorSettingsTests
|
||||
LegacyOperatorSettingsFolderKind.Resource,
|
||||
resourceDirectory);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(
|
||||
LegacyOperatorFolderValidationFailure.InvalidRuntimeCatalog,
|
||||
result.Failure);
|
||||
Assert.Null(result.Folder);
|
||||
AssertPathFree(result.WarningMessage, resourceDirectory);
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(Path.GetFullPath(resourceDirectory), result.Folder!.CanonicalPath);
|
||||
Assert.Null(result.WarningMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResourceValidation_RejectsAnUnsafeDatabaseIniWithoutReadingIt()
|
||||
public void ResourceValidation_DoesNotInspectDatabaseIni()
|
||||
{
|
||||
using var directory = new TemporaryDirectory();
|
||||
var resourceDirectory = directory.CreateDirectory("Res");
|
||||
WriteValidResourceFiles(resourceDirectory);
|
||||
Directory.CreateDirectory(Path.Combine(resourceDirectory, "MmoneyCoder.ini"));
|
||||
|
||||
var result = LegacyOperatorFolderValidator.Validate(
|
||||
LegacyOperatorSettingsFolderKind.Resource,
|
||||
resourceDirectory);
|
||||
|
||||
Assert.False(result.IsValid);
|
||||
Assert.Equal(
|
||||
LegacyOperatorFolderValidationFailure.RequiredFileUnsafe,
|
||||
result.Failure);
|
||||
Assert.Null(result.Folder);
|
||||
AssertPathFree(result.WarningMessage, resourceDirectory);
|
||||
Assert.True(result.IsValid);
|
||||
Assert.Equal(Path.GetFullPath(resourceDirectory), result.Folder!.CanonicalPath);
|
||||
Assert.False(result.Folder.DatabaseIniDetected);
|
||||
Assert.Null(result.WarningMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -59,16 +59,11 @@ public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BothFoldersAndAllActiveIniContractsAreValidatedBeforeSetup()
|
||||
public void BothFolderSelectionsValidateOnlyCanonicalPaths()
|
||||
{
|
||||
var setup = File.ReadAllText(Path.Combine(
|
||||
AppRoot,
|
||||
"FirstRunSetupWindow.xaml.cs"));
|
||||
var settings = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyApplication",
|
||||
"LegacyOperatorSettings.cs"));
|
||||
|
||||
Assert.Contains(
|
||||
"LegacyOperatorSettingsFolderKind.Scene",
|
||||
@@ -78,13 +73,8 @@ public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
"LegacyOperatorSettingsFolderKind.Resource",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"validation.Folder.DatabaseIniDetected",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"new LegacyIniDatabaseOptionsLoader",
|
||||
setup,
|
||||
Assert.DoesNotContain("MmoneyCoder.ini", setup, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyIniDatabaseOptionsLoader", setup,
|
||||
StringComparison.Ordinal);
|
||||
var clearCuts = setup.IndexOf(
|
||||
"_cutsDirectory = null;",
|
||||
@@ -112,62 +102,55 @@ public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
" \"Res\"",
|
||||
setup.ReplaceLineEndings("\n"),
|
||||
StringComparison.Ordinal);
|
||||
|
||||
foreach (var ini in new[]
|
||||
{
|
||||
"종목.ini",
|
||||
"업종_코스피.ini",
|
||||
"업종_코스닥.ini",
|
||||
"해외.ini",
|
||||
"환율.ini",
|
||||
"지수.ini",
|
||||
"종목비교.ini"
|
||||
})
|
||||
{
|
||||
Assert.Contains($"\"{ini}\"", settings, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SetupUsesArgumentListAndPinsTheVerifiedLocalK3dAtTheDefaultEndpoint()
|
||||
public void SetupPersistsOnlyTheSelectedPathsWithoutStartingAChildProcess()
|
||||
{
|
||||
var setup = File.ReadAllText(Path.Combine(
|
||||
AppRoot,
|
||||
"FirstRunSetupWindow.xaml.cs"));
|
||||
|
||||
Assert.Contains(
|
||||
"startInfo.ArgumentList.Add(argument);",
|
||||
var saveSettings = RequiredIndex(
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("\"-NoFolderPicker\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-DatabaseIniPath\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ConfigureDevelopmentLive\"", setup, StringComparison.Ordinal);
|
||||
"var saved = store.Save(current with");
|
||||
var saveBinding = RequiredIndex(
|
||||
setup,
|
||||
"SaveRuntimeBinding(repositoryRoot, runtimeRoot);");
|
||||
Assert.True(saveSettings < saveBinding);
|
||||
Assert.Contains(
|
||||
"private const string DefaultPlayoutHost = \"127.0.0.1\";",
|
||||
"var store = new LegacyOperatorSettingsStore();",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"private const int DefaultPlayoutPort = 30001;",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("\"-PinRegisteredK3D\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ReplaceRuntimeBinding\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ReplaceDatabaseProfile\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains("\"-ReplaceLiveConfig\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"result.LocalConfigurationReplacementRequired",
|
||||
"SceneDirectory = cutsDirectory",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"await ConfirmLocalConfigurationReplacementAsync()",
|
||||
"ResourceDirectory = resourceDirectory",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"-ReplaceK3DPin\"", setup, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\"-SkipBuild\"", setup, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Directory.Build.local.props",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"File.Move(temporary, destination, overwrite: true);",
|
||||
setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("ProcessStartInfo", setup, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Process.Start", setup, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("RunSetupAsync", setup, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("powershell.exe", setup, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("dotnet", setup, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("ConfigureDevelopmentLive", setup,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("PinRegisteredK3D", setup, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("DatabaseIniPath", setup, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SourceOnlyBuildEmbedsOnlyItsOwnClonePathForTheReviewedSetupScript()
|
||||
public void SourceOnlyBuildEmbedsOnlyItsOwnClonePathForTheLocalBinding()
|
||||
{
|
||||
var project = XDocument.Load(Path.Combine(
|
||||
AppRoot,
|
||||
@@ -283,4 +266,11 @@ public sealed class LegacyFirstRunSetupNativeContractTests
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
|
||||
private static int RequiredIndex(string value, string marker)
|
||||
{
|
||||
var index = value.IndexOf(marker, StringComparison.Ordinal);
|
||||
Assert.True(index >= 0, $"Required marker was not found: {marker}");
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +203,9 @@ public sealed class LegacyOperatorSettingsWebContractTests
|
||||
Assert.Contains("databasePollingSeconds", App, StringComparison.Ordinal);
|
||||
Assert.Contains("LegacyOperatorFolderValidator.TryValidate", SettingsHost,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("CanReadSelectedDatabaseProfile", SettingsHost,
|
||||
Assert.DoesNotContain("CanReadSelectedDatabaseProfile", SettingsHost,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("LegacyIniDatabaseOptionsLoader", SettingsHost,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("ResolveDatabaseHealthLabel", SettingsHost,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
Reference in New Issue
Block a user