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,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`은 전역 이름 하나가 아니라 각 화면의
서로 다른 바인딩이다.