feat: restore legacy drag events and Tornado launch integration

This commit is contained in:
2026-07-15 15:35:25 +09:00
parent 6e0c275fdd
commit 06a16e5741
32 changed files with 3096 additions and 101 deletions

View File

@@ -0,0 +1,130 @@
# Visual Studio 2026 개발 Tornado Live 실행
## 목적과 안전 기본
이 문서는 개발 PC의 Visual Studio 2026에서 `MBN_STOCK_WEBVIEW.LegacyParityApp`
명확한 시작 대상으로 실행하고, 개발 Tornado2에 연결하는 방법을 설명한다.
저장소의 송출 기본값과 Release 동작은 계속 `DryRun`이다. 개발 Live는 다음 세 조건이
동시에 맞는 **Debug 프로세스 한 회차**에서만 기존 Live 게이트를 설정한다.
1. 시작 인자가 정확히 `--development-live` 하나이다.
2. 컴파일된 앱이 Debug 빌드이다.
3. 아래 Git 제외 로컬 승인 파일이 존재하며 엄격한 스키마 검사를 통과한다.
조건이 하나라도 다르면 환경 변수를 설정하지 않는다. 로컬 파일이 없거나 읽기 권한이
없고, JSON 또는 해시 형식이 잘못된 경우에도 시작은 fail-closed로 진행되어 기존
`playout.local.json`의 안전 기본인 `DryRun`을 사용한다.
## Visual Studio 시작 대상과 프로필
공유 솔루션 실행 프로필은 저장소 루트의
`MBN_STOCK_WEBVIEW.slnLaunch`에 있으며 다음 프로젝트만 시작한다.
```text
src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj
```
`Debug|x64``Release|x64`의 MSIX Deploy 대상도 루트 프로토타입이 아니라 이 프로젝트이다. Visual
Studio의 시작 드롭다운에는 다음 두 프로필이 표시된다.
| 순서 | 프로필 | 동작 |
|---:|---|---|
| 1 | `MBN_STOCK_WEBVIEW.LegacyParityApp - Development Live (Package)` | `--development-live`를 전달한다. 유효한 로컬 승인 파일이 있을 때만 Live를 구성한다. |
| 2 | `MBN_STOCK_WEBVIEW.LegacyParityApp - Explicit DryRun (Package)` | 개발 Live 인자를 전달하지 않는다. |
개발 PC에서 Live를 사용할 때는 공유 실행 프로필 `Legacy Parity App (VS F5)`, 구성
`Debug`, 플랫폼 `x64`, 첫 번째 Development Live 패키지 프로필을 선택하고 F5를 누른다.
DryRun 확인은 두 번째 프로필을 선택한다. 앱은 단일 인스턴스이므로 프로필을 바꿀 때
기존 인스턴스를 정상 종료한 뒤 다시 실행해야 한다.
패키지된 WinUI 3 full-trust 실행에서는 Visual Studio가 프로세스 명령줄에 인자를 전달해도
`LaunchActivatedEventArgs.Arguments`가 빈 문자열일 수 있다. 부트스트랩은 activation 인자가
비어 있을 때에만 `Environment.GetCommandLineArgs()`가 정확히 `exe + 인자 1개`인 경우 그
하나를 사용한다. 인자가 없거나 둘 이상이면 합치거나 일부를 무시하지 않고 Live 요청을
거부한다. Release 빌드는 이 fallback으로 정확한 인자를 얻더라도 Debug-only gate에서
승인 파일을 읽지 않고 기존 `DryRun` 설정을 유지한다.
## 로컬 승인 파일
실제 파일 경로는 다음과 같다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\playout.development-live.local.json
```
실제 파일명은 `.gitignore`에 포함되어 있다. 저장소의
`Config\playout.development-live.example.json`을 구조 참고용으로 사용하되, 예제의
해시 placeholder는 실행 가능한 값이 아니다. 실제 승인 파일은 정확히 다음 다섯 속성만
가진다.
```json
{
"schemaVersion": 1,
"mode": "Live",
"authorization": "I_AUTHORIZE_LIVE_PROGRAM_OUTPUT_FOR_THIS_LAUNCH",
"nativeSha256": "<독립적으로 승인한 x64 K3D native DLL의 64자리 SHA-256>",
"interopSha256": "<독립적으로 승인한 x64 K3D Interop DLL의 64자리 SHA-256>"
}
```
검사 규칙은 다음과 같다.
- 파일 크기는 1~4096바이트이다.
- 파일과 `%LOCALAPPDATA%` 아래의 중간 디렉터리는 reparse point가 아니다.
- JSON 주석, trailing comma, 중복 속성, 알 수 없는 속성, 대소문자가 다른 속성을
허용하지 않는다.
- `schemaVersion`은 숫자 `1`, `mode`는 정확히 `Live`, `authorization`은 위 문자열과
정확히 일치해야 한다.
- 두 해시는 공백 없는 64자리 16진수여야 한다. 경로나 DLL은 Git에 기록하지 않는다.
- 파일 없음, 잘못된 경로, 공유 위반, I/O 또는 읽기 권한 오류는 모두 같은 제한된
`authorization-unavailable` 결과로 처리하며 경로와 값은 로그에 표시하지 않는다.
현재 설치 파일의 해시를 그 자리에서 계산했다는 이유만으로 승인 값으로 사용해서는 안
된다. 벤더 배포본 또는 기존에 독립 검수된 증적과 대조한 값을 사용한다. 파일 ACL은 현재
개발 사용자와 관리자만 수정할 수 있도록 유지한다.
## 기존 playout.local.json
기존 설정 파일 경로는 그대로이다.
```text
%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Config\playout.local.json
```
이 파일의 `mode``DryRun`으로 유지한다. Development Live 부트스트랩이 성공한 해당
Debug 프로세스에서만 `MBN_STOCK_PLAYOUT_MODE=Live`를 process scope로 덮어쓴다.
기존 이중 게이트를 유지하기 위해 로컬 설정에는 `trustedLiveOutputEnabled: true`
명시되어야 한다.
실제 cue의 `PlayoutCue.SceneName`은 builder의 대표 SceneCode가 아니라 활성 cut alias다.
전체 이관 UI를 개발 PGM에서 검증할 때 `testSceneAllowlist`에는
`LegacySceneRuntimeCoverage.ExpectedCutCodes`와 동일한 다음 45개 alias를 사용한다.
```text
5001, 5006, 5011, 5016, 50160, 5023, 5024, 5025, 5026, 5029,
5032, 5037, 5068, 5070, 5072, 5074, 5076, 5077, 5078, 5079,
5080, 5081, 5082, 5083, 5084, 5085, 5086, 50860, 5087, 5088,
6001, 6067, 8001, 8002, 8003, 8018, 8032, 8035, 8040, 8046,
8051, 8056, 8061, 8067, N5001
```
`8010``8086`은 활성 cue alias가 아니므로 넣지 않는다. 승인 Cuts에 종속 asset이 없는
장면은 allowlist에 있더라도 기존 asset 검증에서 계속 차단된다.
## 프로세스 범위 적용 순서
엄격한 파일 검사가 끝난 뒤 다음 값을 process scope에만 적용한다.
1. 승인된 native SHA-256 pin
2. 승인된 Interop SHA-256 pin
3. `MBN_STOCK_PLAYOUT_MODE=Live`
4. Live authorization — 항상 마지막에 설정
중간 환경 쓰기가 실패하면 기존 값을 복구하고 Live authorization을 새로 설정하지 않는다.
`EnvironmentLiveAuthorization``MainWindow``IPlayoutEngine`을 만들 때 값을 한 번만
캡처하므로, 부트스트랩은 반드시 `new MainWindow()`보다 먼저 실행된다.
Release 빌드에서는 `--development-live`가 전달되어도 로컬 승인 파일을 읽지 않고 환경을
변경하지 않는다. 이 개발 편의 경로는 기존 운영 Live 승인 경로나 안전 게이트를 대체하지
않는다.

View File

@@ -0,0 +1,212 @@
# 원본 WinForms Designer 이벤트 동등성 감사
감사 기준일은 2026-07-15이다. 원본 `C:\Users\MD\source\repos\MBN_STOCK_N`의 커밋
`309286309eaf20b58172247263f575753a860700`을 읽기 전용으로 조사했다. 대상은 실제로
Designer가 구독하는 이벤트이며, 선언만 있고 구독되지 않은 메서드는 수에 넣지 않았다.
Designer 구독은 186개이고, 같은 핸들러를 여러 컨트롤에 연결한 구독을 화면별로 합치면
171개다. 이 문서의 상세 목록은 그 171개를 한 번씩 모두 분류한다.
## 분류 기준
- `IMPL`: 원본의 활성 업무 동작이 C# 소유 상태/워크플로, 엄격한 WebView 브리지 intent,
또는 네이티브 playout 경로로 이관됐다.
- `DEAD`: 원본 메서드가 비어 있거나 전부 주석이거나 즉시 반환한다. 새 명령을 만들지 않는
것이 원본과 같다.
- `VISUAL`: WinForms 전용 paint/draw/focus/resize 동작이며 DOM, CSS, 브라우저 기본 동작으로
대체됐다.
- `DRAG`: 원본 drag/mouse 순서를 Web drag-and-drop과 C# 소유 순서 변경으로 이관한 묶음이다.
- `SAFETY`: 원본과 의도적으로 다르다. 기존 송출 안전 정책 때문에 자동 `StopAll`을 넣지 않는다.
## 집계
| 화면 | Designer 구독 | 고유 핸들러 | IMPL | DEAD | VISUAL | DRAG | SAFETY |
|---|---:|---:|---:|---:|---:|---:|---:|
| UC1 | 9 | 9 | 3 | 2 | 4 | 0 | 0 |
| UC2 | 4 | 4 | 3 | 1 | 0 | 0 | 0 |
| UC3 | 18 | 18 | 14 | 4 | 0 | 0 | 0 |
| UC4 | 15 | 15 | 13 | 2 | 0 | 0 | 0 |
| UC5 | 20 | 9 | 6 | 3 | 0 | 0 | 0 |
| UC6 | 13 | 13 | 12 | 1 | 0 | 0 | 0 |
| UC7 | 7 | 6 | 5 | 1 | 0 | 0 | 0 |
| AList | 3 | 3 | 3 | 0 | 0 | 0 | 0 |
| EList | 3 | 3 | 3 | 0 | 0 | 0 | 0 |
| FSell | 3 | 3 | 3 | 0 | 0 | 0 | 0 |
| GraphE | 15 | 15 | 14 | 1 | 0 | 0 | 0 |
| PList | 7 | 7 | 6 | 1 | 0 | 0 | 0 |
| ThemeA | 13 | 13 | 12 | 1 | 0 | 0 | 0 |
| VIList | 10 | 10 | 10 | 0 | 0 | 0 | 0 |
| MainForm | 46 | 43 | 24 | 3 | 6 | 9 | 1 |
| **합계** | **186** | **171** | **131** | **20** | **10** | **9** | **1** |
UC5는 20개 구독이 9개 핸들러를, UC7은 7개 구독이 6개 핸들러를, MainForm은
46개 구독이 43개 핸들러를 공유한다. 따라서 구독 수와 고유 핸들러 수가 다르다.
## 171개 핸들러 상세 증거
`@숫자`는 각 원본 `*.Designer.cs`의 이벤트 구독 줄이다.
### UC1 — `Control/UC1.Designer.cs`
- `IMPL (3)`: `treeView_DoubleClick@51`, `chE_CheckedChanged@73`, `UC1_Load@84`
- `DEAD (2)`: `treeView_BeforeSelect@49`, `treeView_AfterSelect@50`
- `VISUAL (4)`: `treeView_BeforeCollapse@46`, `treeView_BeforeExpand@47`,
`treeView_DrawNode@48`, `treeView_MouseDown@52`
### UC2 — `Control/UC2.Designer.cs`
- `IMPL (3)`: `btnswap_Click@75`, `listbox_DoubleClick@106`, `UC2_Load@126`
- `DEAD (1)`: `listbox_SelectedIndexChanged@105`
### UC3 — `Control/UC3.Designer.cs`
- `IMPL (14)`: `btnswap_Click@154`, `txtSearch_KeyPress@220`, `listBox_DoubleClick@232`,
`btnsearch_Click@249`, `txtj_KeyPress@314`, `btnsearch2_Click@328`,
`FpSpread2_CellDoubleClick@396`, `btnAdd_Click@470`, `btn4_Click@531`,
`btn3_Click@545`, `btn2_Click@559`, `btn1_Click@573`, `listBox1_DoubleClick@720`,
`UC3_Load@741`
- `DEAD (4)`: `txtSearch_KeyDown@219`, `listBox_SelectedIndexChanged@231`,
`FpSpread2_CellClick_1@395`, `FpSpread_CellClick@641`
### UC4 — `Control/UC4.Designer.cs`
- `IMPL (13)`: `btn1_Click@288`, `txts_KeyDown@297`, `btn2_Click@311`,
`btn3_Click@325`, `btn4_Click@339`, `FpSpread1_CellClick@406`,
`FpSpread1_CellDoubleClick@407`, `btnpre_Click@483`, `btnnow_Click@497`,
`timer1_Tick@659`, `btnnow_6_Click@673`, `btnnow_12_Click@687`, `UC4_Load@752`
- `DEAD (2)`: `FpSpread1_DoubleClick@408`, `FpSpread2_CellClick@564`
### UC5 — `Control/UC5.Designer.cs`
- `IMPL (6)`: `btn1_Click@497`, `btnsearch2_Click@571`, `txtj_KeyPress@581`,
`btnsearch1_Click@747`, `txts_KeyPress@757`, `UC5_Load@1007`
- `DEAD (3)`: `txtj_TextChanged@580`, `txts_TextChanged@756`, `FpSpread_CellClick@939`
### UC6 — `Control/UC6.Designer.cs`
- `IMPL (12)`: `btlistadd_Click@263`, `txtSearch_KeyPress@284`,
`listBox_DoubleClick@295`, `btnsearch_Click@312`, `btnsave_Click@365`,
`btndel_Click@379`, `FpSpread1_CellClick@456`, `FpSpread2_CellClick@570`,
`FpSpread2_KeyDown@571`, `btnDelete_Click@648`, `btnAdd_Click@662`, `UC6_Load@673`
- `DEAD (1)`: `tabp6_1_Paint@331`
- 이벤트 구독과 별도로 원본 `FpSpread2.AllowDragDrop = true`
`Control/UC6.Designer.cs:519`에 있다. 이는 `DRAG` 경로의 전문가 추천행 재정렬로 이관한다.
### UC7 — `Control/UC7.Designer.cs`
- `IMPL (5)`: `btnsearch_Click@185`, `txts_KeyPress@194`, `btn4_Click@219`,
`FpSpread1_CellClick@301`, `UC7_Load@369`
- `DEAD (1)`: `groupBox3_Enter@161`
### AList — `Form/AList.Designer.cs`
- `IMPL (3)`: `btnok_Click@64`, `btncn_Click@88`, `AList_Load@124`
### EList — `Form/EList.Designer.cs`
- `IMPL (3)`: `btncn_Click@56`, `btnok_Click@82`, `EList_Load@137`
### FSell — `Form/FSell.Designer.cs`
- `IMPL (3)`: `btncn_Click@109`, `btnok_Click@125`, `FSell_Load@337`
### GraphE — `Form/GraphE.Designer.cs`
- `IMPL (14)`: `upSearch_Click@220`, `txt1_KeyPress@229`, `downSearch_Click@244`,
`FpSpread1_CellClick@312`, `FpSpread1_CellDoubleClick@313`, `btndel_Click@402`,
`btnall_Click@417`, `btnAdd_Click@432`, `txtSearch_KeyPress@453`,
`listBox_DoubleClick@464`, `btnsearch_Click@481`, `btadd_Click@531`,
`btnok_Click@1274`, `GraphE_Load@1318`
- `DEAD (1)`: `GraphE_KeyPress@1319`
### PList — `Form/PList.Designer.cs`
- `IMPL (6)`: `btnadd_Click@55`, `listBox_DoubleClick@79`, `btncn_Click@95`,
`button3_Click@111`, `button1_Click@127`, `PList_Load@139`
- `DEAD (1)`: `listBox_SelectedIndexChanged@78`
### ThemeA — `Form/ThemeA.Designer.cs`
- `IMPL (12)`: `btncancel_Click@246`, `btnok_Click@260`, `FpSpread_KeyDown@353`,
`btn4_Click@422`, `btn3_Click@436`, `btn2_Click@450`, `btn1_Click@464`,
`btnC_Click@479`, `listBox_DoubleClick@521`, `txtSearch_KeyPress@530`,
`btnsearch_Click@547`, `Theme_Load@569`
- `DEAD (1)`: `listBox_SelectedIndexChanged@520`
### VIList — `Form/VIList.Designer.cs`
- `IMPL (10)`: `txtSearch_KeyPress@188`, `btnok_Click@202`, `btn4_Click@391`,
`btn3_Click@405`, `btn2_Click@419`, `btn1_Click@433`, `listBox_DoubleClick@456`,
`btnsearch_Click@473`, `btncancel_Click@487`, `VILIST_Load@528`
### MainForm — `MainForm.Designer.cs`
- `IMPL (24)`: `tabControl1_SelectedIndexChanged@1062`, `btnPrepare_Click@1465`,
`PlayList_CellClick@1723`, `PlayList_KeyPress@1726`, `ckback_CheckedChanged@1818`,
`btnback_Click@1854`, `btntdelete_Click@1869`, `btndelete_Click@1884`,
`btnload_Click@1899`, `button5_Click@1914`, `btndown_Click@1930`, `btnup_Click@1946`,
`checkBox1_CheckedChanged@1986`, `btnTake_Click@2018`, `txtSearch_KeyPress@2047`,
`b1_Click@2108`, `button9_Click@2207`, `btnNext_Click@2410`,
`btnTakeout_Click@2428`, `timer1_Tick@2433`, `timer3_Tick@2443`,
`Form1_Load@2470`, `MainForm_KeyDown@2472`, `MainForm_KeyUp@2473`
- `DEAD (3)`: `PlayList_LeaveCell@1722`, `GridView1_RowLeave@2081`,
`FpSpread2_CellDoubleClick@2280`
- `VISUAL (6)`: `panel6_Paint@1426`, `GridView1_CellEnter@2080`,
`listBox_DrawItem@2189`, `timer2_Tick@2438`, `MainForm_Paint@2471`,
`MainForm_Resize@2474`
- `DRAG (9)`: `tab_DragOver@1063`, `tab_MouseDown@1064`, `tab_MouseMove@1065`,
`tab_MouseUp@1066`, `PlayList_DragDrop@1724`, `PlayList_DragOver@1725`,
`listView1_MouseDown@2349`, `listView1_MouseMove@2350`, `listView1_MouseUp@2351`
- `SAFETY (1)`: `MainForm_FormClosing@2469`
## ThemeA `중복확인` 동등성
원본 `Form/ThemeA.cs:84-109`는 입력 이름과 현재 KRX/NXT 선택을 기준으로 정확히 같은
테마명을 조회하고 `이미 사용중인 테마명입니다.` 또는 `사용 가능한 테마명입니다.`를 표시한다.
새 구현은 다음 경로를 사용한다.
1. `Web/index.html``operator-catalog-name-check`가 원본 `중복확인` 버튼이다.
2. Web은 이름만 담은 `check-operator-catalog-theme-name` intent를 보낸다. market, code,
table 이름 같은 DB 권한 정보는 보내지 못한다.
3. `LegacyBridgeProtocol`이 닫힌 payload와 128자 canonical text를 검증한다.
4. `LegacyOperatorController.CheckOperatorCatalogThemeNameAsync`
`LegacyOperatorCatalogWorkflowController.CheckThemeNameAvailabilityAsync`를 호출한다.
5. 워크플로는 C#이 소유한 현재 KRX/NXT 대상과 exact title을
`IThemeCatalogPersistenceService.ThemeNameExistsAsync`에 전달한다. 구현은 선택한 provider의
`SB_LIST`에 parameterized `COUNT(*)` SELECT를 정확히 한 번 실행하며 mutation executor를
호출하지 않는다. 128자 테마명도 잘림 없는 exact query로 처리한다.
6. 조회 결과를 확정할 수 없으면 `Indeterminate`로 표시하며 DB 쓰기를 하지 않는다.
원본 쿼리는 edit 중인 현재 `SB_CODE`를 제외하지 않는다. 따라서 제목을 바꾸지 않은 기존
테마에서 `중복확인`을 누르면 자기 행 때문에 `이미 사용중인 테마명입니다.`가 표시된다. 새 구현도
고객의 원본 우선 요구에 따라 KRX/NXT 모두 이 동작을 그대로 보존한다.
검증은 `LegacyThemeCatalogPersistenceServiceTests`, `LegacyOperatorCatalogWorkflowTests`,
`LegacyOperatorCatalogBridgeTests`, `LegacyOperatorCatalogParityWebTests`에 있다.
KRX와 NXT의 같은 이름을 서로 혼동하지 않는지,
create의 중복/사용 가능, edit의 자기 행 중복, 128자 parameter binding, 결과 불명 상태,
엄격한 payload, Web 버튼 및 native dispatch를 확인한다.
## 의도적인 종료 안전 차이
원본 `MainForm.cs:751-780`은 종료 확인 뒤 `KAScenePlayer.StopAll()`, `KAScene.Unload()`,
500ms 대기, 현재 프로세스 강제 종료를 수행한다. 새 앱은 이를 그대로 이관하지 않는다.
- `MainWindow.CloseConfirmation.cs:88-119`는 PREPARED/PROGRAM, pending callback,
`OutcomeUnknown`을 사용자에게 명시하고 종료 과정에서 TAKE OUT을 자동 실행하지 않는다.
- `MainWindow.Playout.cs:778-823`은 on-air 장면, pending callback, quarantine,
`OutcomeUnknown`이 없을 때만 bounded disconnect를 시도한다.
- 이 차이는 기존 안전 게이트의 `결과 불명 시 반복 금지``명시적 송출 명령` 정책을 지키기 위한
활성 안전 요구사항이다. 동등성 작업에서 완화하거나 자동 `StopAll`로 바꾸지 않는다.
## 기타 감사 메모
- 원본 `MainForm.Designer.cs:1399`에서 PREPARE가 들어 있는 `panel3.Visible = false`다.
새 Web UI도 PREPARE를 일반 조작 화면에서는 숨기고 네이티브/승인된 검증 경로는 유지한다.
- 원본의 단일 인스턴스 기대는 `LegacyParityApp/App.xaml.cs`
`AppInstance.FindOrRegisterForKey`로 구현됐다.
- 원본 `timer3_Tick`의 Tornado 상태 감시는 `IPlayoutEngine` 뒤의
`TornadoPlayoutEngine` monitor/reconnect 상태 기계로 격리돼 있다.
- 이벤트 이름이 여러 화면에서 같더라도 화면별 메서드는 별개로 셌다. 예를 들어
`btn1_Click`, `btnsearch_Click`, `listBox_DoubleClick`은 전역 이름 하나가 아니라 각 화면의
서로 다른 바인딩이다.

View File

@@ -13,15 +13,19 @@
WebView2에 표시하는 x64 Windows 데스크톱/MSIX 앱이다.
초기 프로토타입 문서의 “미이관 버튼 disabled”, “Tornado2에 연결하지 않음”, “DB write
없음”은 현재 상태가 아니다. 원본 UI와 클릭 이벤트는 LegacyParityApp → strict bridge →
C# workflow로 연결됐고, 개발 DB write 9개 시나리오와 설치된 MSIX의 개발 PGM 최종
통합까지 검증했다. 기본 송출 모드는 계속 `DryRun`이다.
없음”은 현재 구현 상태가 아니다. 원본 UI 이벤트는 LegacyParityApp → strict bridge →
C# workflow로 연결되고 개발 DB write 9개 시나리오의 기존 증적도 있다. 다만 현재 병렬
변경이 합쳐진 signed Release MSIX 설치·DryRun 실행, 실제 마우스/CDP UI와 제한된
HELLO/BYE 연결은 2026-07-15에 확인했다. scene/PREPARE/TAKE/PGM은 실행하지 않았으므로
전체 방송 송출 성공으로 판정하지 않는다. 기본 송출 모드는 계속 `DryRun`이다.
## 상태 정의
| 상태 | 의미 |
|---|---|
| `완료` | 현재 LegacyParityApp 코드와 해당 자동/통합 검증에 반영됨 |
| `완료` | 현재 LegacyParityApp 코드와 표에 명시한 자동/통합 검증에 반영됨. package/Live 여부는 각 행의 증적 범위로 별도 판정 |
| `부분 실측` | 현재 package 또는 Live의 표에 명시한 범위만 실제 확인했으며 scene/PGM 전체 통합으로 확대할 수 없음 |
| `현재 빌드 실측 제외` | 구현 경로는 있으나 현재 병렬 변경을 합친 package-context/CDP/PGM 결과를 아직 이 감사의 성공 증거로 채택하지 않음 |
| `외부자산 필요` | 코드로 만들 수 없는 Git/MSIX 밖의 승인 영상·배경이 제공되지 않음 |
| `외부 데이터 정합성` | 현재 DB view/data가 원본 규칙의 fresh identity를 공급하지 않아 추정 없이 차단함 |
@@ -33,25 +37,61 @@ C# workflow로 연결됐고, 개발 DB write 9개 시나리오와 설치된 MSIX
| LF-002 | P0 | cut/asset coverage | 완료 | active alias 45개 `.t2s` 존재. alias missing/unsafe 0, asset unsafe 0, asset missing 14를 분리 보고 | 누락 자산 자체는 LF-001/LF-003 |
| LF-003 | P0 | `s5006` 큐브 배경 | 외부자산 필요 | `Video\큐브배경.vrv` 1개가 승인 Cuts에 없음. builder 누락이 아님 | 제공 후 s5006 DryRun/PGM |
| LF-004 | P0 | 실제 개발 DB write | 완료 | GraphE 4 + PList/AList 2 + ThemeA KRX/NXT 2 + EList 1, 총 9개가 create/save→fresh readback→delete→absence PASS, 잔여 0 | 운영 DB write를 뜻하지 않음 |
| LF-005 | P0 | 개발 Tornado2/PGM 최종 통합 | 완료 | 설치된 최신 MSIX에서 `5001` PREPARE/TAKE IN/자동 갱신 → playlist NEXT `5074` 1/2 → Page NEXT 2/2 → TAKE OUT, Network Monitoring·PGM·BYE 확인 | 14개 외부 영상 자산 scene은 제공 후 별도 PGM |
| LF-005 | P0 | 개발 Tornado2/PGM 최종 통합 | 부분 실측 | 현재 signed Release 설치·DryRun 실행과 실제 UI를 확인. 별도 Live에서 HELLO request/success 각 1회와 BYE 1회만 확인했고 scene/PREPARE/TAKE/DB write는 0 | scene PREPARE/TAKE/PGM 전체 통합과 14개 외부 영상 자산 scene은 별도 |
| LF-006 | P0 | 원본 35 scene | 완료 | 원본 inventory와 source 기준선 35/35, DTO→mutation→runtime matrix 통과 | 모든 scene의 실제 PGM 완료로 확대 해석 금지 |
| LF-007 | P0 | PageN/NEXT | 완료 | 5·6·12개 단위, 최대 20페이지, 빈/경계/마지막 부분 페이지 테스트와 개발 PGM에서 `5074` 9종목의 5+4 Page NEXT·다음 행 NEXT 확인 | 없음 |
| LF-007 | P0 | PageN/NEXT | 완료 | 5·6·12개 단위, 최대 20페이지, 빈/경계/마지막 부분 페이지와 같은 scene/다음 행 구분을 자동 테스트로 검증 | 현재 package/PGM 실측은 LF-005와 함께 판정 |
| LF-008 | P0 | 원클릭 TAKE IN | 완료 | 보이는 TAKE IN/F8 한 번으로 fresh page load 후 PREPARE 1회→TAKE IN 1회. native PREPARE는 진단용으로 유지하되 hidden | unknown에서 재시도 금지 |
| LF-009 | P0 | C# 권위/strict bridge | 완료 | Web은 intent만 전송. `LegacyBridgeProtocol` allowlist와 `LegacyOperatorController` snapshot이 DB identity·playlist·scene·송출 상태의 권위 | 새 intent에도 동일 경계 유지 |
| LF-010 | P1 | 10개 탭과 action inventory | 완료 | 10탭, fixed 328, 업종 44, 종목 31, 비교 target 24/action 9를 폐쇄형으로 고정 | 동적 DB 행은 별도 검증 |
| LF-011 | P1 | UC4/UC6/UC7 클릭 | 완료 | 테마·전문가·거래정지는 원본 단일 클릭. UC4/UC6 행은 fixed tree handler와 독립, stale generation 거부 | 없음 |
| LF-012 | P1 | GraphE/FSell/VI | 완료 | GraphE CRUD·찾기·정렬·행 double-click, FSell/VI 확인 시 save+playlist append+close, 취소 경계 이관 | s5025/VI 전용 PGM은 LF-005 |
| LF-013 | P1 | PList/AList | 완료 | 목록·생성·save/load/delete·행 double-click 이관, 개발 DB write/readback/cleanup PASS | 기존 blocked raw row는 추정 복원 금지 |
| LF-014 | P1 | ThemeA/EList | 완료 | 검색·신규·편집·삭제·종목 구성·순서·Delete key 이관, KRX/NXT/expert 개발 DB 시나리오 PASS | scene PGM은 LF-005 |
| LF-014 | P1 | ThemeA/EList | 완료 | 검색·신규·편집·삭제·종목 구성·Delete key, ThemeA exact-title 중복확인, EList opaque `rowId` drag 재정렬 이관. 실제 UI에서 ThemeA 500건 중복 read와 EList 5행 reorder 확인 | scene PGM은 LF-005 |
| LF-015 | P1 | UC5 해외 | 완료 | 해외업종·해외종목·해외지수 조회와 playlist 경로 이관 | s6001 13개 영상은 LF-001 |
| LF-016 | P1 | F2/F3 기본 배경 | 외부자산 필요 | sibling background root, picker, background-none와 trusted path 검사는 완료. 이 장비에 `기본.vrv`가 없음 | 제공 전 fail closed |
| LF-017 | P1 | 시스템 종료 확인 | 완료 | package-context에서 기본 취소 확인창 중립 문구·종료/취소 두 버튼을 대조하고 종료 1회, BYE 1회, 자동 TAKE OUT/반대 명령 없음 확인 | 없음 |
| LF-017 | P1 | 시스템 종료 확인 | 완료 | 기본 취소 확인창, 중립 문구, 종료/취소 두 버튼과 자동 TAKE OUT/반대 명령 금지 경계를 코드·자동 테스트로 유지 | 현재 Release package-context 회귀는 LF-005와 함께 판정 |
| LF-018 | P1 | 원본 비교쌍/수동 파일 | 완료 | `종목비교.dat` 8쌍 1회 import·재시작 marker, FSell 3×5행과 VI 9항목 trusted parser/store 검증 | 원본 파일은 계속 읽기 전용 |
| LF-019 | P1 | 실제 DB read→DTO→mutation | 완료 | Oracle/MariaDB 필수 scene selector 33/33, 전체 query 58건 PASS | 외부 asset/PGM 성공과 별도 |
| LF-020 | P2 | FarPoint 대체 | 완료 | Web table로 바꾸되 선택·정렬·이동·single/double-click·저장 이벤트를 strict C# intent로 재현 | 활성 Print/Excel export는 원본에도 없음 |
| LF-020 | P2 | FarPoint 대체 | 완료 | 실제 마우스로 playlist 다중행 reorder·checkbox 차단·탭 release 전 dragover 교환·EList reorder 확인. row drop 버블링의 cut append와 Chromium checkbox-origin target 문제를 수정하고 DOM 회귀로 고정 | 활성 Print/Excel export는 원본에도 없음 |
| LF-021 | P2 | 패키지 단일 인스턴스·모달 격리 | 완료 | 두 번째 activation 전달, 강제 종료 없음. modal에서 F2/F3/F8/Escape와 playlist key 격리 | 새 MSIX 전체 회귀 |
| LF-022 | P2 | 영구 운영 로그/bridge 오류 | 완료 | bounded/redacted log와 toast, malformed payload가 송출 상태를 바꾸지 않음 | authoritative Network Monitoring과 함께 판독 |
| LF-023 | P1 | NXT 저장 playlist restore | 외부 데이터 정합성 | stale code를 제목/current identity로 재검증하는 닫힌 경로는 완료. 현재 `v_all_stock` 결과가 비어 preview를 fail closed | DBA/data 정합성 없이는 direct-table 추정 금지 |
| LF-024 | P0 | Designer 이벤트 전수 감사 | 완료 | 원본 Designer 구독 **186개**, 화면별 고유 핸들러 **171개**를 IMPL 131/DEAD 20/VISUAL 10/DRAG 9/SAFETY 1로 전수 분류 | 상세 줄 단위 근거는 `LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md` |
| LF-025 | P0 | VS Development Live 시작 구성 | 완료 | `.slnLaunch`와 패키지 프로필이 LegacyParityApp을 시작 대상으로 고정. 실제 Release에서 `--development-live`가 무시돼 DryRun, TCP/Network 변경 0임을 확인 | Live는 exact argument + Debug + strict local authorization을 계속 요구 |
## 이벤트·drag 감사 상세
[`LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md`](LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md)는 원본
커밋의 `*.Designer.cs`를 읽기 전용으로 조사해 186개 구독과 171개 고유 핸들러를 줄 단위로
대조한다. 선언만 있고 구독하지 않는 메서드는 제외하며, `DEAD`/`VISUAL`은 새 활성 업무
intent를 만들지 않는 것이 원본과 동등하다. 종료의 자동 `StopAll`만 기존 안전 정책 때문에
의도적으로 이관하지 않는 `SAFETY` 항목이다.
현재 `DRAG` 구현은 다음 세 C# 권위 경로를 포함한다.
- playlist 행 drag는 선택된 비연속 행도 현재 표시 순서를 보존한 한 블록으로 임의
before/after 위치에 옮긴다. 체크박스에서 시작한 drag와 PREPARED/PROGRAM/pending/unknown
상태의 변경은 차단한다.
- 시장 탭은 drop 때가 아니라 dragover 대상에 들어갈 때 교환한다. source/target identity와
예상 두 인덱스가 C# 현재 순서와 일치할 때만 적용해 중복 메시지가 순서를 되돌리지 않는다.
- 원본 UC6 `FpSpread2.AllowDragDrop`은 ThemeA/EList 편집행의 opaque `rowId` drag로
이관했다. Web은 play index나 DB identity를 결정하지 않는다.
ThemeA의 원본 `중복확인`도 이름만 담은 strict intent로 연결했다. C#이 현재 KRX/NXT를
결정하고 선택 provider의 `SB_LIST`에 exact title parameterized read를 한 번 수행한다.
결과 불명은 `Indeterminate`이며 write나 추정 fallback이 없다. 원본처럼 edit 자기 행을
제외하지 않아 변경하지 않은 이름은 중복으로 판정된다.
## Development Live strict local authorization
[`DEVELOPMENT_LIVE_VISUAL_STUDIO.md`](DEVELOPMENT_LIVE_VISUAL_STUDIO.md)의 시작 구성은
정확한 `--development-live` 인자와 Debug 빌드에서만 Git 제외 로컬 승인 파일을 읽는다.
승인 파일은 `%LOCALAPPDATA%` 아래에서 reparse가 아닌 경로, 최대 4096바이트, 정확한 다섯
속성, canonical JSON, exact authorization 문자열과 독립 승인된 native/Interop SHA-256을
모두 만족해야 한다. Live authorization은 환경 설정의 마지막 단계이며 일부 적용 실패 시
기존 값을 복구한다. Release, 인자 불일치, 파일 없음/오류/알 수 없는 속성은 Live를 구성하지
않는다. 기존 `trustedLiveOutputEnabled`, alias allowlist, asset 검사와 송출 안전 게이트는
그대로 중첩된다.
## DB 검증 상세
@@ -133,17 +173,12 @@ redacted 형태로 보고한다.
## 최종 증적 기록
확정된 증적과 최신 전체 회귀의 기록 위치
[`LEGACY_PARITY_VALIDATION_20260715.md`](LEGACY_PARITY_VALIDATION_20260715.md)다.
다음 값은 추측하거나 이전 package 결과를 재사용하지 않는다.
| 항목 | 상태 |
|---|---|
| Debug/Release x64 전체 테스트 개수 | **PASS** — .NET 각 2,394/2,394, Web 435/435 |
| 새 Release x64 MSIX SHA-256·서명 | `FC4EEE1509675E643D2B4CB435E279CAC4611B28534048D9B9EE611E361E0E1F`, Valid, `CN=Comtrophy` |
| 새 package-context 실행 | **PASS** — DryRun과 개발 Live, 정상 종료 |
| 개발 Tornado2 Network Monitoring/PGM 결과 | **PASS**`5001``5074` 1/2 → 2/2 → TAKE OUT/BYE |
| Gitea 반영 commit | **PASS** — 최종 이관·검증 문서 포함 commit을 `origin/main`에 fast-forward push |
현재 구현 증적과 최종 검증 범위
[`LEGACY_PARITY_VALIDATION_20260715.md`](LEGACY_PARITY_VALIDATION_20260715.md)에 기록한다.
병렬 변경을 모두 합친 .NET Debug/Release x64는 각각 2,446/2,446, Web Node는 438/438이며
Visual Studio 2026 Debug/Release x64 빌드는 경고 0·오류 0이다. 새 signed Release x64 MSIX의
hash·서명·설치 실행, 실제 CDP UI와 제한된 HELLO/BYE 결과도 현재 값으로 기록했다. 단,
scene/PREPARE/TAKE/PGM을 실행하지 않은 현재 연결 회차를 전체 방송 송출 성공으로 확대하지
않으며 과거 package hash나 PGM 로그를 현재 결과로 재사용하지 않는다.
원본 저장소는 최종 검증과 배포 이후에도 계속 읽기 전용으로 유지한다.

View File

@@ -1,7 +1,10 @@
# LegacyParityApp 이관 검증 증적 — 2026-07-15
이 문서는 현재 `MBN_STOCK_WEBVIEW.LegacyParityApp`의 구현·통합 최종 검증 결과를
한곳에 모은다. 원본
이 문서는 현재 `MBN_STOCK_WEBVIEW.LegacyParityApp`의 구현 증적과 지금 채택할 수 있는
검증 경계를 한곳에 모은다. 2026-07-15의 signed Release MSIX 설치·실행, 실제 마우스/CDP
UI 동작과 제한된 Live 연결 실측은 아래 범위로 기록한다. 병렬 변경 전체의 최종
Debug/Release 및 Web 회귀 수는 확정했지만, scene PREPARE/TAKE IN/PGM 출력은 이 회차의
증적으로 확정하지 않는다. 원본
`C:\Users\MD\source\repos\MBN_STOCK_N`은 모든 조사와 테스트에서 읽기 전용으로
취급했다.
@@ -15,23 +18,79 @@
| Web 경계 | `LegacyBridgeProtocol`의 strict JSON intent allowlist; Web은 DB identity, scene alias, playlist 또는 송출 상태를 확정하지 않음 |
| 벤더 경계 | COM/K3D/Tornado 구현은 `IPlayoutEngine` 뒤에 격리하고 STA 큐와 안전 게이트 유지 |
| 기본 송출 모드 | `DryRun`; Live는 별도 개발 설정, 프로세스별 승인, allowlist와 명령 예산 필요 |
| VS Development Live | 정확한 `--development-live` + Debug + Git 제외 strict 로컬 승인 파일이 모두 맞을 때만 process-scope Live 설정; Release와 오류 경로에서는 이 bootstrap이 Live를 새로 설정하지 않음 |
| 불명확 결과 | timeout/`OutcomeUnknown`에서 자동 재시도·반대 명령·추측 cleanup 금지 |
## 원본 UI와 클릭 이벤트
| 범위 | 현재 증적 |
|---|---|
| Designer 전수 감사 | 원본 Designer 구독 **186개**, 화면별 고유 핸들러 **171개**를 IMPL/DEAD/VISUAL/DRAG/SAFETY로 전수 분류 |
| 업무 탭 | 원본 10개 탭, identity 기반 활성화와 순서 변경 |
| 종목 영역 | 모든 탭에서 유지되는 종목 검색, 결과, 31개 컷과 playlist 추가 |
| 고정/업종/비교 | fixed 328개, 업종 44개, 비교 대상 24개와 action 9개를 폐쇄형 inventory로 고정 |
| 독립 클릭 | UC4 테마, UC6 전문가, UC7 거래정지의 원본 단일 클릭과 UC4/UC6 행별 독립 처리 |
| 전용 입력 | GraphE, FSell, VI, PList, AList, ThemeA, EList를 WebView 화면과 C# intent로 연결 |
| 전용 입력 | GraphE, FSell, VI, PList, AList, ThemeA, EList를 WebView 화면과 C# intent로 연결; ThemeA exact-title 중복확인 포함 |
| 해외 | UC5 해외업종·해외종목·해외지수 조회와 playlist 생성 |
| playlist | 단일/Ctrl/Shift 선택, enabled, 위/아래, 선택/전체 삭제, drag/batch 경계 |
| FarPoint 대체 | Web table로 표현하되 원본 클릭·더블클릭·정렬·이동·저장 이벤트 의미 유지 |
| playlist | 단일/Ctrl/Shift 선택, enabled, 위/아래, 선택/전체 삭제와 비연속 다중 선택 block drag; PREPARED/PROGRAM/pending/unknown 변경 차단 |
| FarPoint 대체 | Web table로 표현하되 원본 클릭·더블클릭·정렬·이동·저장, 탭 dragover 교환과 UC6/EList opaque 행 drag 의미 유지 |
상세 이벤트 대조는 [`OPERATOR_UI_PARITY.md`](OPERATOR_UI_PARITY.md)를 따른다.
### 이벤트·drag·중복확인 구현 증적
줄 단위 원본 근거는
[`LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md`](LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md)에 있다.
186개 구독/171개 고유 핸들러 수에는 선언만 있고 구독되지 않은 메서드를 넣지 않았다.
- playlist drag는 opaque source/target `rowId`와 before/after만 Web에서 보내며, C#이 현재
선택 집합을 표시 순서대로 한 블록으로 이동한다. 체크박스 dragstart는 거부한다.
- 시장 탭은 dragover 대상 진입 시 source/target identity와 예상 인덱스를 보낸다. C# 현재
순서가 예상값과 일치할 때만 교환하므로 같은 전달의 재처리는 no-op이다.
- UC6의 `FpSpread2.AllowDragDrop`은 ThemeA/EList 편집 행의 opaque `rowId` 재정렬로
이관했다. Web이 DB identity나 play index를 정하지 않는다.
- ThemeA `중복확인`은 이름 하나만 strict intent로 받고 C#이 현재 KRX/NXT provider에서
exact title parameterized read를 수행한다. 불명 결과는 `Indeterminate`이고 write나
fallback이 없다. edit 자기 행을 제외하지 않는 원본 규칙도 유지한다.
### 2026-07-15 실제 마우스/CDP UI 검증
설치한 현재 Release package의 실제 WebView 화면에서 다음 운영자 입력을 확인했다.
- `삼성` 검색 결과는 **63개**였고 `삼성출판사`를 선택했다. 컷 3개를 선택한 뒤 한 번의
drop으로 playlist에 정확히 3행을 추가했다.
- playlist 여러 행을 실제 마우스로 선택해 한 블록으로 재정렬했고, enabled 체크박스에서
시작한 drag는 행 이동을 만들지 않았다.
- 시장 탭은 마우스 버튼을 놓기 전 dragover 대상 진입 시 순서가 바뀌었다.
- EList 편집기에서 추천행 **5개**의 순서를 drag로 변경했다.
- ThemeA 목록 **500건**을 불러온 상태에서 `중복확인` exact-title read가 성공했다.
이 과정에서 playlist 행 drop이 조상 `playlist-drop-zone`으로 버블링해 아직 선택된 컷을
추가로 append하던 문제가 발견됐다. drop zone은 이제 exact `legacy-selected-cuts`
`text/plain` token만 허용하고 playlist row custom MIME와 외부 text drop은 무시한다.
따라서 행 재정렬은 `reorder-playlist-rows`만 만들고 `drop-selected-cuts`를 만들지 않는다.
또한 Chromium은 체크박스를 눌러 시작한 경우에도 `dragstart.target`을 가장 가까운 draggable
조상 행으로 보고할 수 있어 단순 `event.target.closest("input")` 검사만으로는 부족했다.
pointerdown 때 checkbox origin의 행 ID를 기억하고 이어지는 row dragstart를 거부하며,
pointerup/pointercancel/dragend에서 상태를 지우도록 수정했다. thin-client 회귀 테스트는
이 Chromium target 형태, 다중 선택 보존, row drop의 ancestor 무시, 외부 text 거부와 exact
selected-cut token의 단일 처리를 실제 DOM event 순서로 고정한다.
### Development Live 시작 구성 증적
공유 `.slnLaunch`와 package launch profile은 LegacyParityApp을 시작 대상으로 고정한다.
[`DEVELOPMENT_LIVE_VISUAL_STUDIO.md`](DEVELOPMENT_LIVE_VISUAL_STUDIO.md)의 strict local
authorization은 `%LOCALAPPDATA%` 아래 Git 제외 파일에 정확한 다섯 속성, 최대 4096바이트,
canonical JSON, exact authorization 문자열과 독립 승인 native/Interop SHA-256을 요구한다.
reparse, 알 수 없는/중복 속성, 파일 및 환경 설정 오류는 fail closed하며 authorization은
process scope에서 마지막에 설정된다. 이 부트스트랩은 기존 trusted Live 이중 게이트,
scene allowlist, asset 검사와 timeout/OutcomeUnknown 정책을 대체하지 않는다.
실제 signed Release MSIX에 `--development-live`를 전달해도 compile-time Debug gate 때문에
승인 파일을 적용하지 않고 `DryRun`으로 실행되는 것을 확인했다. 이 Release DryRun 회차에는
Tornado TCP 연결이 없었고 Network Monitoring 내용도 변하지 않았다.
## 송출 흐름과 장면
| 항목 | 결과 |
@@ -54,6 +113,7 @@
| ThemeA KRX write | **PASS** — create, preview/readback, delete, absence |
| ThemeA NXT write | **PASS** — create, preview/readback, delete, absence |
| EList write | **PASS** — expert create, preview/readback, delete, absence |
| ThemeA 중복확인 read | **PASS** — 500건 목록 상태에서 현재 market의 exact title 조회; DB write 없음 |
개발 DB write는 총 9개 충돌 격리 시나리오다. production service/executor를 사용했으며
인증정보나 DB 원문을 출력하지 않았다. write 결과가 불명확한 경우에는 재시도와 cleanup을
@@ -71,54 +131,50 @@
누락 자산은 Git 또는 MSIX에 임의로 넣지 않는다. 제공되면 같은 trusted-root,
0-byte, reparse, root-escape 검사를 통과한 뒤 해당 scene만 다시 검증한다.
## 개발 Tornado2와 PGM 실측
## 2026-07-15 Tornado2 Live 연결 실측
2026-07-15 개발 Tornado2 `2.7.5.49 x64 DSX10.0 S.R2`의 단일 `30001/tcp`
listener와 K3D x64 등록·라이선스를 고정한 뒤, 설치된 MSIX package-context 앱에서 다음
시퀀스를 실행했다.
아래 시각은 개발 장비 로컬 시간(KST)이다. strict local authorization을 통과한 제한된 Live
회차에서 **14:28:28 HELLO request 1회와 success 1회**, **14:43:10 BYE 1회**를 Network
Monitoring에서 확인했다. 이 회차의 scene 명령, PREPARE, TAKE 계열 명령과 DB write는
각각 **0건**이다.
1. 삼성전자 검색 → `5001` playlist 추가
2. 원본과 같은 fixed leaf **더블클릭**으로 VI 수동 목록 9종목 조회 → `5074` 2페이지 추가
3. `PREPARE 5001 page 1``TAKE IN``OnScenePlayed` → 자동 갱신 정확히 1회
4. `NEXT`로 다음 playlist 항목 `5074 page 1/2` → 자동 갱신 정확히 1회
5. 같은 scene `Page NEXT``5074 page 2/2`의 마지막 4종목 송출
6. `TAKE OUT All``IDLE` → 정상 종료 확인 → `BYE`
종료 후 보관한 Network Monitoring 원문은 **300,007자**, SHA-256
`45858322F60C135858002EB6243D0474F51F1965E23D2332D397C3A3B5DB8049`다. 이 증적은 현재
코드의 Live CONNECT/handshake/DISCONNECT 경로만 확인한다. scene load/mutation,
PREPARE, TAKE IN, NEXT/Page NEXT, TAKE OUT 또는 PGM 화면 출력을 검증했다는 뜻이 아니다.
첫 실측과 화면 증적용 known-outcome 재현은 모두 `OutcomeUnknown=false`로 끝났다. 불명확
결과 뒤 명령 재시도는 없었다. Network Monitoring 최종 원문은 1,863줄,
299,616문자, SHA-256
`7CDB183A2518EF47E5CC3819C2EB14C06492DE0C8E018350A0633E0FBBB5B994`다. 두 실측을
합쳐 `LOAD_SCENE` 4/4, `BEGIN_TRANSACTION` 12/12, `VALUE_SET` 258/258,
`VISIBLE` 568/568, `SCENE_PREPARE` 12/12, `PLAY` 10/10, `STOPAL` 2/2,
`UNLOAD_SCENE` 4/4가 요청/성공으로 일치했고 `SCENE_PLAYED` 10회를 수신했다. 정상
종료에서는 `BYE` 요청 1회가 기록됐고 이후 established 연결과 앱/CDP process는 0개다.
별도의 Release DryRun 회차에서는 `--development-live`가 무시됐고 TCP 연결이 생성되지
않았으며 Network Monitoring 최종 내용도 변하지 않았다. 이는 Debug-only bootstrap 경계를
확인한 결과이지 Release Live 송출 결과가 아니다.
화면 증적은 300프레임 중 72프레임이 검은 기준 화면과 달랐다. `5001` 삼성전자 화면,
`5074` page 1의 5종목, page 2의 4종목을 직접 확인했고 TAKE OUT 뒤 기준 검은 화면으로
복귀했다. 프레임 원본과 Network Monitoring 원문은 실제 컷·운영자료를 Git에 넣지 않는
정책에 따라 로컬 `artifacts/final-validation-20260715`에만 보관한다.
## 현재 package와 범위 회귀 기록
## 최종 전체 회귀 기록
아래 값은 모든 병렬 변경과 fixed-leaf 더블클릭 회귀 테스트가 합쳐진 뒤 다시 실행한
최종값이다.
| 항목 | 최종 결과 |
| 항목 | 2026-07-15 확인 결과 |
|---|---|
| Debug x64 전체 빌드·테스트 | **PASS** — .NET 2,394/2,394, VS 2026 `ParityDebug|x64`, 경고 0·오류 0 |
| Release x64 전체 빌드·테스트 | **PASS** — .NET 2,394/2,394, VS 2026 `Release|x64`, 경고 0·오류 0 |
| Web/LegacyParity 전체 테스트 | **PASS** — 47파일, 435/435 (`Test-WebPlayout` 415 + thin-client 20) |
| Release x64 MSIX 경로·버전 | `artifacts/final-validation-20260715/package/MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64_Test/MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64.msix`, `0.1.0.0`, 15,200,545 bytes |
| 새 MSIX SHA-256·서명 | `FC4EEE1509675E643D2B4CB435E279CAC4611B28534048D9B9EE611E361E0E1F`, **Valid**, `CN=Comtrophy` |
| 설치된 package-context 실행 | **PASS**`Wickedness.MBNStockWebView.LegacyParity_0.1.0.0_x64__qbv3jkvsn3aj0`, DryRun 전체 흐름과 Live 개발 PGM 실행, 정상 종료 |
| 개발 Tornado2 Network Monitoring | **PASS** — HELLO, transaction/mutation/query/prepare/play, event, unload, STOPAL, BYE와 연결 해제 확인 |
| 개발 PGM PREPARE → TAKE IN → NEXT/Page NEXT → TAKE OUT | **PASS**`5001``5074` 1/2 → `5074` 2/2 → 검은 화면 복귀 |
| Gitea main commit/push | **PASS** — 이 최종 검증 문서를 포함한 커밋을 `origin/main`에 fast-forward push |
| signed Release x64 MSIX | **15,213,188 bytes**, SHA-256 `357E17426C45FF516592EE0177D0F95502A98AF084759348ABC191BA5ED21FD5` |
| 설치·실행 | 설치된 package-context 앱 실행 확인 |
| Release `--development-live` | Debug-only gate에 따라 무시되고 `DryRun`; TCP 연결과 Network Monitoring 변경 없음 |
| 실제 마우스/CDP UI 범위 | 삼성 63건 검색→삼성출판사→3컷 단일 drop, playlist multi-row reorder, checkbox drag 차단, tab dragover 교환, EList 5행 reorder, EList 수량 입력칸 origin drag 차단, ThemeA 500건과 중복확인 read 확인 |
| 제한된 Live Network 범위 | HELLO request/success 각 1회와 BYE 1회; scene/PREPARE/TAKE/DB write 0 |
| .NET Debug/Release x64 | 각 **2,446/2,446**, 실패·건너뜀 0, 경고·오류 0 |
| Web Node 회귀 | **438/438**, 실패·취소·건너뜀·TODO 0 |
| Visual Studio 2026 build | VS 18.7.3 / MSBuild 18.7.8, Debug·Release x64 모두 경고 0 / 오류 0 |
playlist drop MIME 경계, checkbox-origin drag와 EList input/button-origin drag 회귀는
thin-client DOM event 테스트로도 고정했다. 설치된 최종 Release package에서 실제 마우스로
EList 수량 입력칸에서 시작한 drag가 순서를 바꾸지 않고, 같은 행의 비입력 영역에서 시작한
drag만 5행 순서를 바꾸는 것도 확인했다. .NET Debug/Release 합계 4,892회와 Web 438회를
모두 통과했지만,
scene PREPARE/TAKE/PGM을 실행하지 않은 이 Live 연결 회차를 전체 방송 송출 PASS로 확대하지
않는다.
## 판정 경계
- 35/35 자동 장면 동등성은 모든 장면의 실제 PGM 출력 완료를 뜻하지 않는다.
- 33/33 DB selector 통과는 외부 영상/배경 자산 준비를 뜻하지 않는다.
- 개발 DB 9개 write 시나리오 통과는 운영 DB에 쓰기를 허용하거나 수행했다는 뜻이 아니다.
- 개발 Tornado2/PGM은 안전 게이트를 유지한 별도 통합 검증 대상이다.
- 현재 signed Release MSIX의 설치·DryRun 실행, 실제 마우스/CDP UI, .NET Debug/Release
각 2,446개와 Web 438개 회귀를 확인했다.
- Live 증적은 HELLO/BYE만 확인한 연결 회차다. scene/PREPARE/TAKE/DB write가 0이므로
PGM 송출 또는 원클릭 TAKE IN 전체 통합 성공으로 판정하지 않는다.
- 원본 저장소, 실제 Cuts, 라이선스와 인증정보는 계속 읽기 전용 또는 Git 외부로 유지한다.

View File

@@ -28,6 +28,16 @@ playlist, PageN과 송출 상태는 `LegacyOperatorController`의 C# snapshot이
- 원본 `Cuts`도 읽기 전용 검증에만 사용하며 수정·복사하지 않는다.
- 비밀번호, 라이선스, 벤더 DLL, 인증서, 실제 컷·영상·배경과 로컬 설정은 Git에 넣지 않는다.
## 원본 Designer 이벤트 전수 감사
원본 `*.Designer.cs`가 실제로 구독하는 이벤트를 전수 조사한 결과는 **186개 Designer
구독, 화면별 중복을 합친 171개 고유 핸들러**다. 선언만 있고 Designer에서 구독하지 않는
메서드는 수에 넣지 않았다. 171개는 `IMPL` 131, `DEAD` 20, `VISUAL` 10, `DRAG` 9,
`SAFETY` 1로 모두 분류했다. 같은 이름의 핸들러라도 화면이 다르면 별도 바인딩으로 센다.
상세 원본 파일·구독 줄·분류 근거는
[`LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md`](LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md)에 있다.
## 원본 실행 action inventory
동적 테마·전문가 DB 행과 전용 편집기의 데이터 행은 아래 고정 수량에 중복해 더하지 않는다.
@@ -61,8 +71,8 @@ playlist, PageN과 송출 상태는 `LegacyOperatorController`의 C# snapshot이
| VIList | 종목 추가·삭제·이동, 5개 단위 page, 확인 시 저장+추가+닫기 | `LegacyManualListsWorkflow` + trusted store | 원본 형식 read와 격리 write 검증 | `s5074` 전용 입력 PGM 별도 |
| PList | 목록, 행 더블클릭 load/save, 확인, 삭제 | `LegacyNamedPlaylistWorkflow` | 개발 DB create/save/read/delete/absence PASS | 복원 항목별 scene PGM 별도 |
| AList | 다음 코드와 프로그램명 신규 생성 | named playlist workflow에 통합 | 위 named playlist 시나리오로 PASS | 해당 없음 |
| ThemeA | KRX/NXT 테마 생성·편집·삭제, 종목 구성·순서 | `LegacyOperatorCatalogWorkflow` | 개발 DB KRX/NXT 각 1개 PASS | scene PGM 별도 |
| EList | 전문가 생성·편집·삭제, 추천 종목·금액·순서 | `LegacyOperatorCatalogWorkflow` | 개발 DB expert 1개 PASS | scene PGM 별도 |
| ThemeA | KRX/NXT 테마 생성·편집·삭제, 이름 중복확인, 종목 구성·순서 | `LegacyOperatorCatalogWorkflow` + exact-title 조회 | 개발 DB KRX/NXT 각 1개 PASS | scene PGM 별도 |
| EList | 전문가 생성·편집·삭제, 추천 종목·금액·드래그 순서 | `LegacyOperatorCatalogWorkflow` + opaque row reorder | 개발 DB expert 1개 PASS | scene PGM 별도 |
UC4/UC6의 결과 행은 고정 action 트리의 click handler에 버블링하지 않는다. UC4 테마,
UC6 전문가와 UC7 거래정지는 원본처럼 **단일 클릭**으로 선택/playlist 동작을 수행하고,
@@ -74,10 +84,10 @@ fixed/industry/comparison 트리의 원본 double-click과 독립적으로 처
|---|---|---|
| `Form1_Load` | 자산·DB·탭·Tornado 초기화 | WebView local UI와 C# composition root 초기화; 기본 `DryRun` |
| `tabControl1_SelectedIndexChanged` | 탭별 최초 데이터 조회 | 10개 identity별 C# activation gate; 최초 진입 1회 load, 재진입 snapshot 유지 |
| 탭 drag 이벤트 | 10개 탭 표시 순서 교환 | identity 기반 drag/키보드 이동; active tab·playlist 불변 |
| 탭 drag 이벤트 | 10개 탭 표시 순서 교환 | dragover 대상 진입 시 identity와 예상 위치를 C#이 검증해 교환; 중복 전달은 no-op, active tab·playlist 불변 |
| 검색 버튼/Enter | 종목 검색 | 원본 KOSPI→KOSDAQ→NXT 순서와 마지막 이름 일치 규칙 유지 |
| 컷 click/double-click/drag | 원본 31컷 선택·추가 | plain/Ctrl/Shift/Ctrl+Shift와 원본 double-click 판정, batch 추가 |
| playlist click/key/buttons | 선택, enabled, 이동, 삭제 | C# playlist authority; PREPARED/PROGRAM/pending/unknown에서 mutation 차단 |
| playlist click/key/buttons/drag | 선택, enabled, 이동, 삭제, 다중 선택 행 이동 | 선택된 행은 원래 순서를 보존한 한 블록으로 임의 위치에 이동; C# playlist authority PREPARED/PROGRAM/pending/unknown mutation 차단 유지 |
| F2/F3 | 배경 선택·없음 | trusted sibling background root만 허용; 누락·reparse·escape fail closed |
| F8 | TAKE IN | idle이면 내부 PREPARE → TAKE IN을 한 번에 실행 |
| NEXT | Page NEXT 또는 다음 행 | 같은 scene PageN 여부를 C#이 판정 |
@@ -91,6 +101,8 @@ FarPoint UI는 Web table/list로 표현한다. 다음 의미는 원본과 같이
- plain/Ctrl/Shift/Ctrl+Shift 선택과 현재 행
- column header 정렬과 현재 표시 순서
- 위/아래 이동, 선택/전체 삭제와 enabled 토글
- playlist의 비연속 다중 선택을 한 블록으로 유지하는 행 drag와 임의 before/after drop
- UC6/EList 추천 행의 opaque `rowId` 기반 drag 재정렬
- single-click과 double-click을 컨트롤별로 구분
- 부모 section 전체 추가는 모든 자식을 먼저 materialize한 뒤 한 번에 commit
- 수동/지원 불가 child가 있으면 부분 추가 없이 전체 차단
@@ -101,6 +113,34 @@ GraphE 행 double-click은 fresh DB record를 다시 확인한 뒤 한 행을
Theme 결과 double-click은 원본의 5행 현재가 경로를 사용한다. PList 행 double-click은
현재 save/load 모드에 맞춰 동작하며, 이름만으로 DB identity를 추정하지 않는다.
### drag 이관 경계
| 원본 동작 | Web 입력 | C# 권위와 중복 안전 |
|---|---|---|
| `PlayList_DragDrop/DragOver` | 내부 custom MIME와 opaque source/target `rowId`, before/after | source가 선택 행이면 현재 선택 전체를 표시 순서대로 이동한다. 체크박스에서 시작한 drag는 거부하고 PREPARED/PROGRAM/pending/unknown에서는 변경하지 않는다. |
| `tab_DragOver/MouseDown/Move/Up` | drop을 기다리지 않고 dragover로 `hover-swap-tabs` 전송 | C# 탭 identity와 `expectedSourceIndex/expectedTargetIndex`가 현재 순서와 일치할 때만 교환하므로 같은 메시지의 중복 전달이 순서를 되돌리지 않는다. |
| UC6 `FpSpread2.AllowDragDrop` | ThemeA/EList 편집행의 내부 custom MIME와 opaque `rowId` | 워크플로가 임의 before/after 위치를 계산한다. 존재하지 않는 ID, 같은 행, 추가 payload 필드는 strict bridge에서 거부하거나 no-op 처리한다. |
실제 마우스 검증에서는 playlist 행 drop이 조상 drop zone으로 버블링해 현재 선택된 컷을
한 번 더 추가하는 문제가 드러났다. 조상은 이제 exact `legacy-selected-cuts` `text/plain`
token만 처리하고 playlist row custom MIME와 외부 text는 무시한다. 따라서 한 row drop에서
reorder와 cut append가 함께 발생하지 않는다.
체크박스 origin도 브라우저 동작을 반영해 보강했다. Chromium의 `dragstart.target`은 실제
pointer가 체크박스에서 시작했어도 draggable 조상 행일 수 있다. pointerdown 때 checkbox
origin row를 기억해 이어지는 dragstart를 거부하고 pointerup/pointercancel/dragend에서 지운다.
회귀 테스트는 이 target 형태, 다중선택 보존, row MIME의 조상 무시, 외부 text 거부와 exact
selected-cut token의 1회 처리를 DOM event 순서로 재현한다.
### ThemeA 이름 중복확인
원본 ThemeA의 `중복확인` 버튼을 `operator-catalog-name-check`로 이관했다. Web은 이름만
`check-operator-catalog-theme-name` intent로 전송하고 KRX/NXT, 코드와 테이블 identity는
C# workflow가 소유한다. 선택된 provider의 `SB_LIST`에 exact title parameterized
`COUNT(*)` 조회를 한 번 수행하며 DB mutation은 실행하지 않는다. 중복, 사용 가능,
결과 불명 상태를 구분하고, 조회 결과를 확정할 수 없으면 쓰기 없이 fail closed한다.
원본처럼 편집 중 자기 행을 제외하지 않으므로 이름을 바꾸지 않은 행은 중복으로 판정된다.
## 원클릭 TAKE IN과 숨김 PREPARE
원본 운영 화면의 핵심 동작은 별도 PREPARE를 먼저 누르는 것이 아니라 F8/TAKE IN 한 번으로
@@ -115,6 +155,46 @@ Theme 결과 double-click은 원본의 5행 현재가 경로를 사용한다. PL
`hidden`이다. TAKE IN 실패·timeout·`OutcomeUnknown`이면 자동 재시도하지 않으며 성공한
prepared ownership을 추측해 지우지 않는다.
## Visual Studio Development Live 시작 경계
공유 `MBN_STOCK_WEBVIEW.slnLaunch`와 패키지 시작 프로필은
`MBN_STOCK_WEBVIEW.LegacyParityApp`을 명시적인 시작 프로젝트로 사용한다. 저장소의
기본값은 계속 `DryRun`이고 이 Development Live 부트스트랩은 Release에서 적용되지 않는다.
Development Live는 정확한 `--development-live`
인자, Debug 빌드, `%LOCALAPPDATA%` 아래 Git 제외 승인 파일의 세 조건을 모두 만족한
프로세스에서만 구성된다.
승인 파일은 정확한 다섯 속성, 최대 4096바이트, canonical JSON, exact authorization
문자열과 독립 검수된 native/Interop SHA-256 두 개를 요구한다. 알 수 없는 속성, 중복 속성,
reparse 경로, I/O 오류 또는 일부 환경 설정 실패는 모두 fail closed한다. Live authorization은
process scope에서 마지막에 설정되고 `MainWindow`/engine 생성 시 한 번 캡처된다. 이 경로는
기존 `trustedLiveOutputEnabled`, scene allowlist, asset 검사와 명령 안전 게이트를 완화하지
않는다. 상세 절차는
[`DEVELOPMENT_LIVE_VISUAL_STUDIO.md`](DEVELOPMENT_LIVE_VISUAL_STUDIO.md)에 있다.
## 2026-07-15 실제 UI·package·연결 검증
설치한 signed Release x64 MSIX는 **15,213,188 bytes**, SHA-256
`357E17426C45FF516592EE0177D0F95502A98AF084759348ABC191BA5ED21FD5`다. 설치와
package-context 실행을 확인했다. Release에 `--development-live`를 전달한 회차는 Debug-only
gate 때문에 `DryRun`으로 남았고 Tornado TCP 연결과 Network Monitoring 변경이 없었다.
실제 WebView 마우스/CDP 입력의 확인 범위는 다음과 같다.
- `삼성` 검색 결과 **63개**, `삼성출판사` 선택, 선택한 컷 3개를 한 번의 drop으로
playlist 3행에 추가
- playlist multi-row block reorder와 enabled 체크박스 origin drag 차단
- mouse release 전에 dragover로 시장 탭 순서 교환
- EList 추천행 **5개** drag reorder. 수량 입력칸에서 시작한 실제 마우스 drag는 순서를
바꾸지 않고, 행의 비입력 영역에서 시작한 drag만 순서를 바꾸는 것을 확인
- ThemeA 결과 **500건**과 exact-title `중복확인` read 성공
별도 승인된 Development Live 회차의 장비 로컬 시간(KST) **14:28:28**에 HELLO request와
success를 각 1회, **14:43:10**에 BYE 1회를 확인했다. scene, PREPARE, TAKE 계열, DB write는
모두 0건이다. 최종 Network Monitoring 원문은 **300,007자**, SHA-256
`45858322F60C135858002EB6243D0474F51F1965E23D2332D397C3A3B5DB8049`다. 이 회차는
Live 연결/해제 경로만 검증하며 scene/PGM 방송 송출 성공을 뜻하지 않는다.
## PageN과 NEXT
| scene | page size | 규칙 |
@@ -167,9 +247,12 @@ create/save → fresh readback → delete → absence를 통과했다.
| GraphE | [LegacyManualFinancialWorkflow.cs](../src/MBN_STOCK_WEBVIEW.LegacyApplication/ManualFinancial/LegacyManualFinancialWorkflow.cs) | [manual-financial tests](../tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/ManualFinancial) |
| PList/AList | [LegacyNamedPlaylistWorkflow.cs](../src/MBN_STOCK_WEBVIEW.LegacyApplication/NamedPlaylist/LegacyNamedPlaylistWorkflow.cs) | [named-playlist tests](../tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/NamedPlaylist) |
| ThemeA/EList | [LegacyOperatorCatalogWorkflow.cs](../src/MBN_STOCK_WEBVIEW.LegacyApplication/OperatorCatalog/LegacyOperatorCatalogWorkflow.cs) | [catalog tests](../tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/OperatorCatalog) |
| Designer 이벤트 감사 | [LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md](LEGACY_DESIGNER_EVENT_PARITY_AUDIT.md) | 186 subscriptions / 171 unique handlers |
| VS Development Live | [DEVELOPMENT_LIVE_VISUAL_STUDIO.md](DEVELOPMENT_LIVE_VISUAL_STUDIO.md) | strict local authorization와 Debug-only bootstrap |
| scene matrix | [SCENE_EQUIVALENCE.md](SCENE_EQUIVALENCE.md) | 35/35 builder, 45 alias, PageN matrix |
| 2026-07-15 증적 | [LEGACY_PARITY_VALIDATION_20260715.md](LEGACY_PARITY_VALIDATION_20260715.md) | 최종 전체 회귀 기록란 포함 |
| 2026-07-15 증적 | [LEGACY_PARITY_VALIDATION_20260715.md](LEGACY_PARITY_VALIDATION_20260715.md) | signed Release, 실제 UI와 제한된 Live 연결 범위 |
최종 Debug/Release 전체 테스트 개수, 새 MSIX SHA-256와 개발 PGM 결과는 병렬 변경을
모두 합친 뒤 위 검증 문서에 기록했다. 이전 package 숫자나 hash를 현재 결과로
재사용하지 않았다.
현재 signed Release MSIX의 hash·크기·설치·DryRun 실행과 위 실제 마우스/CDP UI 범위는
확인했다. 최종 회귀는 .NET Debug/Release x64에서 각각 **2,446/2,446**, Web Node에서
**438/438**, Visual Studio 2026 Debug/Release x64 빌드에서 경고 0·오류 0이다. 다만
HELLO/BYE만 수행한 Live 회차를 scene PREPARE/TAKE/PGM 성공으로 확대하지 않는다.