# 운영자 UI 동등성 인벤토리 기준일: 2026-07-15 ## 제품과 판정 기준 현재 이관 앱은 웹사이트가 아니라 `.NET 8 / C# / WinUI 3 / WebView2` Windows 데스크톱 프로그램인 `MBN_STOCK_WEBVIEW.LegacyParityApp`이다. WebView2는 패키지 안의 로컬 HTML/CSS/JavaScript를 표시한다. Web은 원본 UI를 표현하고 사용자 intent를 전달한다. 원본 이벤트의 상태 전이, DB identity, playlist, PageN과 송출 상태는 `LegacyOperatorController`의 C# snapshot이 권위다. Web 메시지는 `LegacyBridgeProtocol`의 strict JSON allowlist에 없는 필드·값·명령을 거부한다. 이 문서의 판정은 다음을 구분한다. | 판정 | 의미 | |---|---| | UI/이벤트 이관 | 원본 컨트롤과 클릭 이벤트가 실제 LegacyParityApp 화면 → strict bridge → C# workflow로 연결됨 | | 자동 동등성 | 원본 inventory·DTO·mutation·호출 순서·경계값을 자동 테스트로 비교함 | | 개발 DB 통합 | 실제 개발 Oracle/MariaDB에서 create/readback/delete/absence를 확인함 | | 개발 PGM 통합 | 개발 Tornado2 Network Monitoring과 PGM 화면을 함께 확인함 | | 외부자산 필요 | 코드 경로는 있으나 Git/MSIX 밖의 실제 영상·배경이 제공되지 않음 | ## 원본 보존 - 원본 `C:\Users\MD\source\repos\MBN_STOCK_N`은 읽기 전용으로만 분석했다. - 원본 `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 행과 전용 편집기의 데이터 행은 아래 고정 수량에 중복해 더하지 않는다. | 집합 | 원본 UI | 수량 | 현재 경계 | 상태 | |---|---|---:|---|---| | fixed action | MainForm 해외·환율·지수와 UC1 | **328** | `LegacyFixedActionCatalog` + strict action/section intent | 이관 | | 업종 action | UC1 + UC2 | **44** | `LegacyIndustryActionCatalog` + C# section batch | 이관 | | 종목 컷 | MainForm 좌측 검색·컷 | **31** | `LegacyStockCutCatalog` | 이관 | | 비교 action | UC1 + UC3 | **9** | 24개 target + `LegacyComparisonWorkflowController` | 이관 | | 업무 탭 | MainForm TabControl | **10** | identity 기반 활성화·순서 변경 | 이관 | 328개 fixed action 중 수동 전제 6개는 임의 DTO로 만들지 않는다. 개인·외국인·기관 순매도 3개는 FSell의 신뢰된 5행 입력으로, 5단·6종목·12종목 트리의 VI placeholder 3개는 원본 VIList가 만든 `s5074/5074` 5개 단위 PageN으로만 활성화한다. ## 화면 단위 현황 | 원본 화면 | 이관된 핵심 동작 | 현재 구현 | 개발 DB/파일 검증 | 개발 PGM/자산 경계 | |---|---|---|---|---| | MainForm | 종목 검색, 31컷, playlist, 배경, 송출, 단축키, 종료 확인 | LegacyParityApp shell + C# controller | 실제 DB 검색/read smoke PASS | 송출 최종 결과는 검증 문서에 기록 | | UC1 | 328 fixed, 업종·비교 트리, 부모/leaf batch | C# 폐쇄 inventory와 원자적 section materialize | read-only selector 검증 | 장면별 PGM은 별도 | | UC2 | 업종 검색, 두 슬롯 회전·교환, 44 action | industry workflow/controller | Oracle read PASS | 장면별 PGM은 별도 | | UC3 | 비교 대상·검색·쌍 CRUD·9 action | comparison workflow/controller | 원본 8쌍 1회 import·재시작 영속성 검증 | 장면별 PGM은 별도 | | UC4 | KRX/NXT 테마 검색·preview·5/6/12 선택 | theme workflow; 목록 행 단일 클릭 | KRX/NXT selector PASS | 외부 scene 자산과 PGM 별도 | | UC5 | 해외업종·해외종목·해외지수 | overseas workflow/controller | Oracle selector PASS | `s6001` 영상 13개 외부자산 미제공 | | UC6 | 전문가 검색·추천 preview·5행 선택 | expert workflow; 목록 행 단일 클릭 | selector PASS | 장면별 PGM은 별도 | | UC7 | 거래정지 검색·현재/120일 캔들 | trading-halt workflow; 원본 단일 클릭 | Oracle selector PASS | 장면별 PGM은 별도 | | GraphE | 4개 재무 CRUD, 정렬·찾기, 행 더블클릭 playlist 추가 | `LegacyManualFinancialWorkflow` | 개발 DB 4개 테이블 write PASS | 전용 scene PGM 별도 | | FSell | 개인·외국인·기관 각 5행, 확인 시 저장+추가+닫기 | `LegacyManualListsWorkflow` + trusted store | 원본 형식 read와 격리 write 검증 | `s5025` trusted 파일 전제 | | 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` + 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 동작을 수행하고, fixed/industry/comparison 트리의 원본 double-click과 독립적으로 처리한다. ## MainForm 이벤트 매핑 | 원본 이벤트 | 원본 의미 | LegacyParityApp 동작 | |---|---|---| | `Form1_Load` | 자산·DB·탭·Tornado 초기화 | WebView local UI와 C# composition root 초기화; 기본 `DryRun` | | `tabControl1_SelectedIndexChanged` | 탭별 최초 데이터 조회 | 10개 identity별 C# activation gate; 최초 진입 1회 load, 재진입 snapshot 유지 | | 탭 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/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#이 판정 | | Escape | TAKE OUT | modal이 없을 때만 StopAll 기반 TAKE OUT intent | | FormClosing | 종료 확인 | 기본 취소 확인창; 자동 TAKE OUT/반대 명령 없음 | ## FarPoint 대체와 클릭 규칙 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가 있으면 부분 추가 없이 전체 차단 - modal이 열리면 F2/F3/F8/Escape, Delete/Space/Home/End 등 전역 동작 격리 - stale generation, 예상하지 않은 row id, 추가 payload field를 strict bridge에서 거부 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 한 번으로 송출하는 것이다. 현재 보이는 TAKE IN은 idle에서도 활성 조건을 만족하면 다음을 실행한다. 1. playlist selection과 page를 C# snapshot으로 고정 2. DB/신뢰 파일 DTO와 PageN을 fresh load 3. 장면 mutation과 transaction 순서를 적용해 `Prepare` 정확히 1회 4. 성공한 prepared scene에 `TakeIn` 정확히 1회 엔지니어링용 native PREPARE control과 explicit workflow는 테스트를 위해 유지하지만 Web UI에서 `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 | 규칙 | |---|---:|---| | `s5074` | 5 | 테마/전문가/VI 등 5행 경로 | | `s5077` | 6 | 6종목 테마 경로 | | `s5088` | 12 | 12종목 테마 경로 | `pageCount = min(20, ceil(itemCount / pageSize))`를 사용한다. 0행, 정확한 경계, 마지막 부분 페이지, 20페이지 cap, 21페이지 요청 거부, 같은 scene의 `GetPlayingScene` Page NEXT와 다음 enabled playlist 항목 NEXT를 자동 테스트로 비교한다. ## 개발 DB write 증적 실제 개발 Oracle/MariaDB에서 production executor를 사용한 9개 충돌 격리 시나리오가 create/save → fresh readback → delete → absence를 통과했다. | 기능 | 시나리오 | |---|---:| | GraphE `INPUT_PIE/GROW/SELL/PROFIT` | 4 | | PList/AList named playlist | 2 | | ThemeA KRX/NXT | 2 | | EList expert | 1 | | 합계 | **9** | 이는 개발 DB 통합 증거이며 운영 DB write를 수행했다는 뜻이 아니다. write 재시도는 없고, 결과가 불명확하면 cleanup과 다음 시나리오도 실행하지 않는 fail-closed 규칙을 유지한다. ## read-only·scene·asset 증적 - 실제 Oracle/MariaDB 필수 scene selector **33/33**, 전체 query 58건 PASS - 원본 scene builder 자동 기준선 **35/35** - MainForm 도달 runtime 34개 builder와 active alias **45개** - 승인된 Cuts에 45 alias 대응 `.t2s` 존재 - `s5006` `Video\큐브배경.vrv` 1개와 `s6001` 국가 영상 13개는 외부자산 미제공 - sibling `배경\기본.vrv`도 외부자산 미제공 마지막 14개 영상과 `기본.vrv`의 부재는 UI/scene 코드 실패가 아니다. 해당 기능은 자산이 제공될 때까지 COM 전에 차단하며 임의 파일로 대체하지 않는다. ## 구현·테스트 빠른 링크 | 영역 | 현재 실행 구현 | 주요 테스트/근거 | |---|---|---| | WinUI 3 + WebView2 shell | [MainWindow.xaml.cs](../src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.xaml.cs), [Web app](../src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/app.js), [Web layout](../src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/index.html) | [thin client](../tests/LegacyParityWeb/thin-client.test.cjs) | | C# 업무 권위 | [LegacyOperatorController.cs](../src/MBN_STOCK_WEBVIEW.LegacyApplication/LegacyOperatorController.cs) | [LegacyApplication tests](../tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests) | | strict bridge | [LegacyBridgeProtocol.cs](../src/MBN_STOCK_WEBVIEW.LegacyBridge/LegacyBridgeProtocol.cs) | [LegacyBridge tests](../tests/MBN_STOCK_WEBVIEW.LegacyBridge.Tests) | | 송출 | [MainWindow.Playout.cs](../src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.Playout.cs), [playout-ui.js](../src/MBN_STOCK_WEBVIEW.LegacyParityApp/Web/playout-ui.js), [LegacyPlayoutWorkflow.cs](../src/MBN_STOCK_WEBVIEW.Core/Playout/Scenes/LegacyPlayoutWorkflow.cs) | [LegacyPlayoutWorkflowTests.cs](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyPlayoutWorkflowTests.cs) | | 수동 목록 | [MainWindow.ManualLists.cs](../src/MBN_STOCK_WEBVIEW.LegacyParityApp/MainWindow.ManualLists.cs), [LegacyManualListsWorkflow.cs](../src/MBN_STOCK_WEBVIEW.LegacyApplication/ManualLists/LegacyManualListsWorkflow.cs) | [manual-list tests](../tests/MBN_STOCK_WEBVIEW.LegacyApplication.Tests/ManualLists) | | 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) | signed Release, 실제 UI와 제한된 Live 연결 범위 | 현재 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 성공으로 확대하지 않는다.