feat: complete legacy UI and playout parity migration

This commit is contained in:
2026-07-15 13:11:38 +09:00
parent f14800656b
commit 6e0c275fdd
108 changed files with 43738 additions and 560 deletions

View File

@@ -1,148 +1,149 @@
# 원본 기능 재감사 기준표
감사 기준일: 2026-07-12
감사 기준일: 2026-07-15
원본(읽기 전용): `C:\Users\MD\source\repos\MBN_STOCK_N`
마이그레이션 대상: `C:\Users\MD\source\repos\MBN_STOCK_WEBVIEW`
이 문서는 원본 WinForms/FarPoint 프로그램과 현재 WinUI 3/WebView2 프로그램을 다시 대조한 결과다. 원본 저장소와 실제 Cuts는 읽기 전용으로 조사했다. 자동 테스트나 UI 코드가 존재한다는 사실과 실제 운영 환경에서 동등성이 확인됐다는 사실을 구분한다.
## 감사 결론
현재 실행 대상은 `.NET 8 / C# / WinUI 3 / Windows App SDK 1.8 / WebView2` 기반
`MBN_STOCK_WEBVIEW.LegacyParityApp`이다. 이는 웹사이트가 아니라 로컬 Web 자산을
WebView2에 표시하는 x64 Windows 데스크톱/MSIX 앱이다.
초기 프로토타입 문서의 “미이관 버튼 disabled”, “Tornado2에 연결하지 않음”, “DB write
없음”은 현재 상태가 아니다. 원본 UI와 클릭 이벤트는 LegacyParityApp → strict bridge →
C# workflow로 연결됐고, 개발 DB write 9개 시나리오와 설치된 MSIX의 개발 PGM 최종
통합까지 검증했다. 기본 송출 모드는 계속 `DryRun`이다.
## 상태 정의
| 상태 | 의미 |
|---|---|
| `미적용` | 원본 동작의 대응 구현 또는 필수 검증 경로가 아직 없다. |
| `적용 중` | 현재 작업 트리에 구현 또는 검사 보강이 있으나 최종 패키지·실환경 검증까지 끝나지 않았다. |
| `완료` | 현재 코드와 해당 자동 검증에 반영됐고 이번 재감사에서 완료로 판정했다. |
| `외부자산 필요` | 코드만으로 완료할 수 없으며 Git/MSIX 밖의 승인된 자산이나 운영 환경이 필요하다. |
완료 수는 증분 포팅과 최종 회귀 결과에 따라 바뀌므로 아래 표를 단일 기준으로 사용한다. 기존 문서에서 사용하는 `완료`, `통합 완료`, `전체 통과` 표현도 아래 상태와 검증 범위로 다시 해석해야 한다.
| `완료` | 현재 LegacyParityApp 코드와 해당 자동/통합 검증에 반영됨 |
| `외부자산 필요` | 코드로 만들 수 없는 Git/MSIX 밖의 승인 영상·배경이 제공되지 않음 |
| `외부 데이터 정합성` | 현재 DB view/data가 원본 규칙의 fresh identity를 공급하지 않아 추정 없이 차단함 |
## 우선순위별 결과
| ID | 우선순위 | 기능·누락 후보 | 상태 | 근거 파일·메서드 | 현재 판정 | 검증 방법 |
|---|---|---|---|---|---|---|
| LF-001 | P0 | `s6001` 해외지수 영상 자산 | 외부자산 필요 | 현재 `src/MBN_STOCK_WEBVIEW.Core/Playout/Scenes/ReadOnlyPanelSceneBuilders.cs:491-513`, `S6001SceneMutationBuilder.Metadata`/`Index`; 검증기 `src/MBN_STOCK_WEBVIEW.Playout/Configuration/ValidatedPlayoutOptions.cs:375-406`, `ResolveAssetPath` | 해외지수 15개 target이 13개 `Video\20201008_<국가>.vrv`에 의존한다. 감사 시 승인 Cuts의 `Video` 폴더는 비어 있어 이 target들은 DryRun PREPARE에서도 COM 호출 전에 차단된다. 유가·금의 PNG/JPG는 존재한다. | 승인된 파일을 Cuts root 아래에 제공한 뒤 강화된 cut/asset 검사와 s6001 각 target DryRun PREPARE를 실행한다. 실제 PGM은 별도 승인 회차에서만 확인한다. |
| LF-002 | P0 | cut coverage의 장면 종속 asset 검사 | 완료 | `scripts/LegacyCutRequirements.json`, `LegacyCutCoverage.psm1`, `Test-LegacyCutCoverage.ps1`; 테스트 `tests/Scripts/Test-LegacyCutCoverage.Tests.ps1`, `LegacyCutRequirementManifestTests.cs` | 45개 active alias와 23개 장면 종속 asset을 분리해 존재·0바이트·root escape·reparse·manifest 중복을 검사한다. 완전/누락/0바이트/reparse/escape fixture가 자동 테스트에 연결됐다. 실제 승인 Cuts read-only 실행은 alias missing/unsafe 0, asset unsafe 0, asset missing 14로 정확히 실패했다: `s5006` 1개와 `s6001` 13개 영상이다. | 실제 누락 14개가 승인 Cuts에 제공되면 같은 명령을 다시 실행해 `Status=Passed`를 확인한다. 검사기 완료와 외부 자산 준비 완료는 LF-001/LF-003에서 분리한다. |
| LF-003 | P0 | `s5006` 큐브 배경 영상 | 외부자산 필요 | 원본 `MBN_STOCK_N/Scene/s5006.cs:30-31`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Playout/Scenes/ReadOnlyPanelSceneBuilders.cs:31-48`; `docs/PLAYOUT_OPERATIONS.md:81-85` | `Video\큐브배경.vrv`가 승인 Cuts에 없. 기존 운영 문서에는 누락이 기록돼 있으나 파일 제공 전에는 `s5006` PREPARE가 완료된 것이 아니다. | asset 검사 통과 후 s5006 DryRun PREPARE, 승인된 Test/PGM 회차 순으로 검증한다. |
| LF-004 | P0 | 실제 운영 DB write | 미적용 | `docs/OPERATOR_UI_PARITY.md:48-62,167-170,214-218,229-246,278`; GraphE `MainWindow.ManualFinancial.cs`; PList `MainWindow.NamedPlaylists.cs`; ThemeA/EList `MainWindow.OperatorCatalogs.cs` | mutation 계약, fake executor, rollback·OutcomeUnknown 테스트는 존재하지만 GraphE, PList/AList, ThemeA, EList의 운영 Oracle write는 실행하지 않았다. FSell/VI도 격리 저장소 write만 검증했다. 따라서 운영 데이터 쓰기 동등성은 미검증이다. | 승인된 테스트용 식별자와 사전 snapshot을 정하고 create/update/delete를 각 1회 실행한다. 매 단계 후 별도 read-only 조회로 결과를 대조하고, timeout·OutcomeUnknown이면 반복하지 않는다. 운영 데이터에는 임의로 실행하지 않는다. |
| LF-005 | P0 | 전체 장면·운영 UI의 실제 Tornado2/PGM 검증 | 미적용 | `docs/MIGRATION.md:69-74`; `docs/OPERATOR_UI_PARITY.md:48-62`; `docs/PLAYOUT.md:348-366` | 승인된 실제 회차의 범위는 5001/5074와 그 Page/playlist NEXT, refresh, TAKE OUT이다. 자동 builder/DB smoke 결과를 나머지 장면의 PGM 동등성으로 확대할 수 없다. | 장면별 허용 자산과 DB 전제조건을 먼저 통과시킨다. 이후 회차별 계획 SHA-256과 명시적 승인을 받아 Network Monitoring 명령, callback, PGM 화면을 함께 기록한다. |
| LF-006 | P1 | 시스템 종료 확인 | 완료 | `MainWindow.CloseConfirmation.cs`, `EnsureCloseConfirmationAttached`, `OnAppWindowClosing`, `BuildCloseConfirmationMessage`, `DetachCloseConfirmation`; 연결부 `MainWindow.xaml.cs:54-63,101-112,OnClosed`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs:384-408` | 모든 시스템 close를 먼저 취소하고 기본 버튼이 취소인 확인창을 연다. PREPARED/PROGRAM, command pending, OutcomeUnknown에 맞는 경고를 표시하며 종료 과정에서 TAKE OUT이나 반대 명령을 자동 실행하지 않는다. | Web 통합 테스트를 실행하고 패키지 앱에서 idle, prepared/on-air 경고, 취소, 명시적 종료를 수동 확인한다. |
| LF-007 | P1 | 패키지 앱 단일 인스턴스 | 완료 | `App.xaml.cs:9-57`, `OnLaunched`, `OnMainInstanceActivated`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs:72-88` | `AppInstance.FindOrRegisterForKey`가 MainWindow와 DB/WebView/playout runtime 생성 전에 실행된다. 두 번째 인스턴스는 activation을 primary로 전달한 뒤 종료하며 기존 프로세스를 이름으로 찾아 강제 종료하지 않는다. | 통합 테스트 후 설치된 MSIX를 두 번 실행해 프로세스 하나, 창 하나, 기존 창 활성화를 확인한다. |
| LF-008 | P1 | 운영자 모달의 전역 단축키 격리 | 완료 | `Web/app.js:6808-6845`, `hasOpenOperatorModal`, `isolateOperatorModalShortcut`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs:410-429` | GraphE, FSell/VI, ThemeA/EList 등 modal이 열려 있으면 F2/F3/F8/Escape, Home/End/Delete/Space, Ctrl+K가 workspace 송출·playlist handler로 전달되지 않는다. F8/Escape에는 modal을 먼저 닫으라는 안내를 표시한다. | Web 통합 테스트 후 각 modal 입력 필드에서 해당 키를 눌러 playout command와 playlist mutation이 발생하지 않는지 확인한다. |
| LF-009 | P1 | PList의 기존 한국어 `DC_TITLE`/`LIST_TEXT` 복원 | 적용 중 | 원본 `MBN_STOCK_N/Form/PList.cs:81-124`, `data_load`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyPListReadAudit.cs`, `tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedPlaylistReadOnlyDbAudit.cs`, `tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedManualRestoreReadOnlyAudit.cs`; KRX/해외종목/manual 전용 restore와 테스트; 상세 `docs/NAMED_PLAYLIST_READ_AUDIT.md` | 실제 Oracle 활성 정의 23개와 2,213행 모두 정확히 7필드이고 원문 재결합, 한국어 제목·필드, enabled, page 공백 제거 및 typed persistence가 일치했다. 기본 감사는 KRX 94행 current-code remap과 해외종목 78/78을 포함해 `selectionMapped=1,581`, 차단 632다. 별도 GraphE fresh 감사가 manual 36행 중 22행을 materialize하고 저장값 무효 9행·identity 중복 5행을 차단해 두 증거의 통합 결과는 `selectionMapped=1,603`, 차단 610이다. 이 수치는 scene data·asset·PREPARE·PGM 성공이 아니다. | 집중 테스트, Release 실제 read-only DB 감사와 전체 `REAL_DB_SMOKE: PASS`. FSell/VI PList 행은 0건이지만 운영 production read는 FSell 3×5행, VI 9항목·2 page이고 파일 전후 불변이다. raw 값·인증정보 출력, DB write, Tornado/PGM 명령은 0건이다. 남은 KRX/NXT/live-data·GraphE identity/data·unique mapper 차단을 해소하고 scene/asset/package/PGM gate를 별도로 통과하기 전에는 전체 복원 완료로 올리지 않는다. |
| LF-010 | P1 | 원본 F2/F3 배경 디렉터리 동등성 | 코드 적용 / 외부자산 필요 | 원본 `MBN_STOCK_N/MainForm.cs:4008-4054`, `btnback_Click`, `ckback_CheckedChanged`; 현재 `MainWindow.Background.cs`, `PlayoutSceneCompositionFactory`, `TrustedPlayoutAssetPath`, `ValidatedPlayoutOptions`; 문서 `docs/TRUSTED_BACKGROUND_ROOT.md` | Cuts gate를 유지한 채 별도 `LegacyBackgroundDirectory`를 추가했고, 생략 시 원본처럼 `SceneDirectory\..\배경`을 계산한다. F2는 유효할 때만 picker 전 `기본.vrv`를 복원하고 F3-on은 항상 기본 파일을 다시 검증한다. root escape·조상/파일 reparse·hardlink·누락·opened-handle final path를 fail closed로 검사하며 Cuts fallback은 없다. 이 장비의 원본 `bin\Debug\배경``기본.vrv`는 실제로 없어 현재 기본 배경은 잠긴다. | Core Debug/Release 1,377/1,377, Playout 387/387과 VS2026 앱 Debug/Release x64 build를 통과했다. signed 1.0.4 package는 누락 상태를 별도 배경 상태로 표시한다. 자산 제공 뒤 F2/F3와 다음 PREPARE mutation/PGM은 승인 회차에서 검증한다. |
| LF-011 | P1 | `종목비교.dat` 기존 저장쌍 이전 | 완료 | 원본 `MBN_STOCK_N/Control/UC3.cs:34-55,181-185`, `UC3_Load`, `Data_Load`, `File_Save`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyComparisonPairImport.cs`, `MainWindow.LegacyComparisonImport.cs`, `Web/comparison-import-workflow.js`, `Web/app.js`; 테스트 `LegacyComparisonPairImportServiceTests.cs`, `comparison-import-workflow.test.cjs`, `comparison-import-app-integration.test.cjs`; 실행 증거 `docs/LEGACY_ONE_TIME_IMPORT_AUDIT.md` | 원본 CP949 358바이트·8행·9필드와 SHA-256 `1EE76BC4…BFC0D2F2`를 고정하고, 고정 read-only source·strict parser·현재 Oracle/MariaDB exact identity·전체 원자 저장·one-time marker를 적용했다. 설치된 signed 1.0.3 package에서 빈 상태 `0 PAIRS`를 확인한 뒤 명시적 버튼을 정확히 한 번 실행해 8쌍을 저장했다. 정상 종료·재시작 후에도 `8 PAIRS`, 원본 순서의 8행과 완료형 `원본 8쌍 가져옴` gate가 유지됐고, 원본 파일 SHA-256은 불변이었다. 원본·DB·Tornado에는 쓰기나 명령을 보내지 않았다. | Core 20/20, Web 12/12, 실제 read-only DB identity 8/8, package 0→8 및 재시작 marker 영속성 PASS. 손상·동명이름·기존쌍 중복·부분 실패는 계속 전체 fail closed이며 package family data를 초기화해 같은 회차를 재현하지 않는다. |
| LF-012 | P1 | FSell/VI 원본 `Data`의 안전한 이전과 설정 문서 | 적용 중 | 원본 `MBN_STOCK_N/MainForm.cs:338-339`, `FSell.cs:31-88`, `VIList.cs:31-34,137-139`; importer `LegacyManualOperatorDataImport`, `LegacyManualOperatorDataImporter`; production read `S5025TrustedManualFileDataSource`, `ViTrustedManualFileStore`; 감사 `tools/MBN_STOCK_WEBVIEW.DbSmoke/NamedManualRestoreReadOnlyAudit.cs`, `docs/NAMED_MANUAL_RESTORE_READ_ONLY_AUDIT.md` | 고정 원본 경로·strict CP949·source/hash 재검증·no-overwrite staged commit·marker-last·OutcomeUnknown quarantine를 유지한다. 격리 fixture는 FSell 15행·VI 9건, aggregate SHA-256 `fd39a363…a2114`, 첫 실행 성공과 두 번째 `AlreadyImported`를 확인했다. 운영 profile은 이미 원본과 byte-identical한 4파일이 있어 importer가 `DestinationNotEmpty`로 차단되며 marker/intent를 만들지 않는다. production opened-handle read는 FSell 3 audience 각각 5행, VI 9항목·2 page와 안정된 두 번째 version을 확인했고 감사 전후 4파일은 불변이다. 현재 PList에는 FSell/VI 행이 0건이다. | 새 빈 signed package profile의 명시적 UI importer 검증은 남아 있다. 현재 운영 profile의 동일 파일을 덮어쓰거나 marker를 임의 생성하지 않는다. PList에 향후 FSell/VI 행이 생기면 같은 fresh source/version gate를 거치며 scene data·asset·PREPARE·PGM은 별도 검증한다. |
| LF-013 | P1 | generic native `bridge-error` 표시 | 완료 | producer `MainWindow.xaml.cs`, `MainWindow.Playout.cs`; consumer `Web/app.js` `handleBridgeError`/`handleNativeMessage`; 테스트 `operator-ui-app-integration.test.cjs` | bounded toast/log로 소비하며 malformed status/quarantine payload에서 playout state를 변경하지 않는다. 전체 Web 413/413과 signed 1.0.4 패키지 반영을 확인했다. | 새 native error producer를 추가할 때마다 같은 bounded consumer 계약을 유지한다. |
| LF-014 | P2 | 영구 운영 로그 | 완료 | 원본 `MBN_STOCK_N/MainForm.cs:84-180`; 현재 `NativeOperatorLogWriter.cs`, `NativeOperatorLogTransitions.cs`, `MainWindow.NativeOperatorLog.cs` | `%LOCALAPPDATA%\MBN_STOCK_WEBVIEW\Logs\Log_MMdd.log`에 폐쇄 event/state만 기록하며 4MiB/31일, 중복 억제, reparse/handle 검증, single terminal/OutcomeUnknown 규칙을 적용한다. signed 1.0.4 package context에서도 Startup→DryRunReady→Oracle/MariaDB Healthy가 기록됐고 playout command·PROGRAM·OutcomeUnknown은 0건이었다. SQL·경로·인증·requestId를 기록하지 않는다. | 운영 회차에서도 로그를 명령 재시도 근거로 사용하지 않고 authoritative 출력과 함께 판독한다. |
| LF-015 | P2 | 호출자가 없는 native bridge handler | 완료 | `MainWindow.xaml.cs`, `OnWebMessageReceived`; 테스트 `tests/Web/operator-ui-app-integration.test.cjs` | Web 호출자가 없던 `open-external``reload` message case를 native allowlist에서 제거했다. 외부 navigation/new-window 처리와 WebView process failure의 내부 reload 복구 메서드는 별도 native event 경로로 유지한다. | 정적 계약 테스트에서 두 message case와 Web 발신 literal이 모두 없음을 확인했다. |
| LF-016 | P2 | 실제 preview 이미지·영상 | 외부자산 필요 | `docs/MIGRATION.md:73-74`; 원본 `MBN_STOCK_N/MainForm.cs:1448-1462,1894-1911,2919-2921` | 현재 preview는 안전한 DTO/mutation 텍스트 중심이다. 실제 이미지·영상 연결은 후속 자산이다. 다만 원본 thumbnail 호출과 watcher는 주석 또는 미호출이므로 활성 원본 동등성의 P0/P1 blocker로 보지 않는다. | 승인된 preview 자산이 생기면 asset root와 노출 정보 검사를 거쳐 패키지 화면에서 확인한다. |
| LF-017 | P1 | 모든 업무 탭에서 계속 보이는 종목 검색·31컷 | 완료 | 원본 `MBN_STOCK_N/MainForm.Designer.cs:2022-2036,2454-2462`; 현재 `Web/index.html`의 독립 `stock-panel`, `Web/app.js``renderStockWorkflow`; 테스트 `operator-ui-app-integration.test.cjs`; package 증거 `docs/RELEASE_1_0_4_AUDIT.md` | 종목 검색·31컷을 catalog panel 밖의 독립 열로 옮겼다. signed 1.0.4 package에서 실제 `삼성` 검색 63건을 확인하고 10개 업무 탭을 모두 방문한 뒤에도 검색어·결과·31개 컷이 유지됐다. package는 계속 DryRun이었다. | Web 413/413, source Web 33개 package byte 일치, 설치 package UI PASS. 향후 새 업무 탭도 같은 독립 패널 계약을 유지한다. |
| LF-018 | P2 | 원본 트리·컷 목록 일괄 추가 제스처 | 적용 중 | 원본 부모 double-click `MBN_STOCK_N/Control/UC1.cs:141-180`; 소스 컷 Ctrl/Shift 선택 `MBN_STOCK_N/MainForm.cs:3427-3525,3534-3549,3635-3643`; 현재 `Web/legacy-batch-selection-workflow.js`, `Web/app.js`; 테스트 `legacy-batch-selection-*.test.cjs` | 종목 31컷의 plain/Ctrl/Shift/Ctrl+Shift 선택, 단일 double-click, `선택 컷 추가`를 포팅했다. 해외·환율·지수 fixed source의 섹션 `전체 추가`는 모든 자식을 먼저 materialize한 뒤 원본 순서로 한 번에 commit하며, 수동/지원불가가 한 건이라도 있으면 부분 추가 없이 전체 차단한다. 실제 브라우저에서 5개 선택, 해외 43개 순서 추가, 수동 혼합 섹션 0건 추가를 확인했다. | signed MSIX 확인 뒤 업종·비교·테마 등 전용 source group의 부모 전체 추가가 원본에서 실제로 필요한 범위를 계속 분리 포팅한다. source drag/drop 자체는 오조작 검증 뒤 결정한다. |
| LF-019 | P1 | 저장 PList의 `테마_NXT` stale code 복원 | 코드 완료 / DB view 정합성 대기 | 원본 `Control/UC4.cs`, `Scene/s5074.cs`, `s5077.cs`, `s5088.cs`; 현재 `src/MBN_STOCK_WEBVIEW.Core/Data/LegacyNamedNxtThemeRestore.cs`, `MainWindow.NamedNxtThemeRestore.cs`, `Web/named-nxt-theme-restore-workflow.js`; 테스트 `LegacyNamedNxtThemeRestoreServiceTests.cs`, `named-nxt-theme-restore-workflow.test.cjs`, `named-nxt-theme-restore-app-integration.test.cjs`; 상세 `docs/NAMED_NXT_THEME_RESTORE.md`, `docs/NXT_THEME_RESTORE_READ_ONLY_AUDIT.md` | 제목 exact lookup, 현재 Maria code, 5/6/12행, 정렬 fallback, 00~12 PRE/13~23 AFTER와 fresh PageN을 닫힌 batch로 복원한다. 실제 48행/24제목은 current identity가 유일하고 저장 code는 전부 stale다. `SB_ITEM` 446행과 NXT master 446행은 존재하지만 원본과 새 앱이 공통으로 쓰는 `v_all_stock` 조인은 0행이어서 48행 모두 `PreviewEmpty`로 차단된다. 통합 PList 결과에서도 `selectionMapped`로 계산하지 않았고 추측성 직접-table fallback을 적용하지 않았다. | Core Debug/Release와 실제 read-only DB smoke PASS, DB write·Tornado/PGM 명령 0건. DBA가 `v_all_stock`의 NXT 포함 의도를 확인해 승인된 변경·rollback으로 복구하거나 직접-table 경로를 별도 승인한 뒤 fresh page preflight/MSIX/PGM을 검증한다. |
| LF-020 | P2 | 원본 초기 업무 화면 `해외` | 완료 | 원본 `MainForm.Designer.cs` 첫 TabPage `해외`; 현재 `Web/index.html`, `Web/app.js`; 테스트 `operator-ui-app-integration.test.cjs` | dashboard는 진단 화면으로 유지하되 초기 active identity/제목/요청을 해외로 맞췄다. signed 1.0.4 package를 새로 실행했을 때 해외만 active였고 실제 해외 DB 상태가 표시됐다. | 후속 탭 추가 시에도 초기 identity를 순번이 아닌 `data-market`으로 유지한다. |
| LF-021 | P2 | 업무 탭 드래그 순서 변경 | 완료 | 원본 `MainForm.cs:3183-3312`; 현재 `Web/market-nav-reorder.js`, `Web/app.js`; 테스트 `market-nav-reorder*.test.cjs` | dashboard는 첫 위치에 고정하고 10개 업무 identity만 drag 교환/Alt+위·아래로 이동한다. active view/DB/playlist는 불변이고 입력·modal·PREPARE 잠금에서 fail closed하며 세션 종료 시 기본 순서로 돌아간다. 실제 브라우저에서 `Alt+Down` 후 active 해외 유지와 reload 초기화를 확인했다. | signed 1.0.4 패키지에 source Web 33개 일치로 반영했다. |
| LF-022 | P2 | GraphE 위/아래 찾기 | 완료 | 원본 `GraphE.cs:338-379,605-611`; 현재 `Web/manual-financial-ui.js/.css`; 테스트 `manual-financial-ui.test.cjs` | `위로 찾기`/`아래로 찾기` 버튼과 Enter 다음 찾기를 포팅했다. trim·대소문자 무시 Contains, 현재 행 전/후, no-wrap, 모든 pending·잠금·hidden 차단을 유지한다. 원본의 `i > 0` 첫 행 누락은 명백한 off-by-one으로 보고 첫 행도 정상 후보에 포함했다. | GraphE 집중 20/20, 전체 Web 413/413과 signed 1.0.4 Web 일치 통과. |
| LF-023 | P2 | ThemeA/전문가 편집기 Delete 키 | 완료 | 원본 `ThemeA.cs`/`UC6.cs`; 현재 `Web/operator-catalog-ui.js`; 테스트 `operator-catalog-ui.test.cjs` | ThemeA와 expert draft의 포커스/선택 행 하나를 기존 `removeDraftItem`으로 삭제하고 input/textarea/contenteditable, modal 밖, 잠금/DB write 중에는 상태를 변경하지 않는다. 삭제 후 inputIndex를 재배열하며 native 요청은 보내지 않는다. | 관련 UI/workflow/bridge 32/32, 전체 Web 413/413과 signed 1.0.4 Web 일치 통과. |
| LF-024 | P2 | GraphE 종목명 헤더 정렬 | 완료 | 원본 `GraphE.cs:258-280`; 현재 `Web/manual-financial-ui.js/.css`; 테스트 `manual-financial-ui.test.cjs` | `종목명 ↕` 첫 click 내림차순/둘째 click 오름차순을 세션 로컬 display order로 적용했다. 선택 snapshot/native/playlist는 불변이고 위/아래 찾기는 현재 표시 순서를 따른다. 새 화면/목록에서 오름차순으로 초기화한다. | 네 모드·한글/ASCII·빈/단일·pending·정렬된 find 집중 20/20, 전체 Web 413/413 통과. |
| LF-025 | P2 | UC7 거래정지 종목명 헤더 정렬 | 완료 | 원본 `Control/UC7.cs:173-188`; 현재 `Web/trading-halt-workflow.js`, `Web/app.js`, `Web/index.html`; 테스트 `trading-halt-name-sort-app-integration.test.cjs` | `종목명 ↕` 첫 click 내림차순/둘째 click 오름차순을 표시 order에만 적용했다. 동명은 market+stockCode로 결정적으로 정렬하고 선택 identity를 유지하며 새 검색은 server 순서로 초기화한다. DB/native/playlist 변경은 없다. | 집중 18/18, 전체 Web 413/413, 실제 브라우저에서 접근 라벨/가로 overflow 0, signed 1.0.4 Web 일치 통과. |
| ID | 우선순위 | 기능 | 상태 | 현재 판정과 증적 | 남은 경계 |
|---|---|---|---|---|---|
| LF-001 | P0 | `s6001` 해외지수 영상 | 외부자산 필요 | builder와 trusted asset 검사는 완료. 승인 Cuts에 국가 영상 13개가 없음 | 제공 후 asset preflight와 해당 scene PGM |
| 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-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-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-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-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-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 추정 금지 |
## 활성 PLAY_LIST 2,213행 read-only 재분류
## DB 검증 상세
운영 Oracle의 활성 프로그램 정의 23개와 연결된 2,213행을 원문을 출력하지 않는
감사로 다시 분류했다. 원본 `PList.data_load`의 page 공백 제거를 제외하면 모든
필드를 그대로 보존했고, 현재 persistence 결과와 2,213/2,213 일치했다.
### Read-only scene smoke
| route | 건수 | 현재 결과 |
실제 Oracle/MariaDB에서 필수 scene selector **33/33**과 전체 query 58건이 성공했다.
이 검증은 실제 DB → 명시 DTO → scene mutation 경로를 증명한다. `.t2s`, 영상·배경 자산,
K3D 명령 또는 PGM 화면 검증을 대신하지 않는다.
### 개발 DB write smoke
production service/executor와 충돌 격리된 임시 식별자를 사용했다.
| 시나리오 | 개수 | 검증 |
|---|---:|---|
| 기존 닫힌 매퍼 | 1,250 | hash 고정 원본 INI와 현재 exact signature로 selection 1,250개 매핑 |
| 업종 fixed 닫힌 매퍼 | 24 | selection 24개 매핑 |
| 비교 fresh identity | 99 | selection 74개 매핑, 시장 identity 유실 25개 차단 |
| 해외지수 candle fresh identity | 61 | selection 61개 매핑 |
| NXT theme fresh identity | 48 | validated preview empty 48 차단 |
| KRX theme fresh identity | 587 | current-code remap 94, identity 없음 271, 저장 행 무효 16, preview empty 193, 지원 불가 action 13 |
| 해외종목 fresh identity | 78 | exact 입력명에서 unique symbol/nation을 correlated materialize해 78개 매핑 |
| GraphE fresh materialization | 36 | 별도 double-read row-version·stock identity·cut 계약으로 22개 매핑, 저장값 무효 9·identity 중복 5 차단. FSell/VI PList행은 0 |
| unique closed mapper 없음 | 30 | 원본 328-action runtime hash와 현재 exact signature 중 단일 후보가 없어 nearest scene 추정 없이 차단 |
| 합계 | 2,213 | 기본 감사 `selectionMapped=1,581`/차단 632; 별도 GraphE proof를 합치면 `selectionMapped=1,603`/차단 610 |
| GraphE `INPUT_PIE`, `INPUT_GROW`, `INPUT_SELL`, `INPUT_PROFIT` | 4 | create, fresh readback, delete, absence |
| PList/AList named playlist | 2 | definition/items save, fresh readback, delete, absence |
| ThemeA KRX | 1 | create, preview/readback, delete, absence |
| ThemeA NXT | 1 | create, preview/readback, delete, absence |
| EList expert | 1 | create, preview/readback, delete, absence |
| 합계 | **9** | **PASS**, 잔여 0 |
`unique=2,213`은 전부 송출 가능하다는 뜻이 아니라 모든 행이 단일 route의
selection 결과 또는 명시적 차단 사유를 가졌다는 뜻이다. `selectionMapped`
장면 데이터·자산, package context, PREPARE 또는 PGM 성공이 아니다. 이들은 집계
밖의 별도 gate다. KRX/NXT theme은
원본 `s5074/s5077/s5088`처럼 제목 exact lookup을 사용하며, 저장 code를 가장 가까운
현재 code로 추정하지 않는다. 비교 25행도 현재 이름이 우연히 유일하더라도 저장
행이 시장을 보존하지 않았으므로 계속 차단한다. 상세 SQL 경계와 결과는
[`NAMED_PLAYLIST_READ_AUDIT.md`](NAMED_PLAYLIST_READ_AUDIT.md)에 기록했다.
write는 자동 재시도하지 않는다. `OutcomeUnknown`이면 cleanup mutation이나 다음 시나리오를
보내지 않는 구현·테스트 경계를 유지한다. 이 결과는 개발 DB 검증이며 운영 DB에 임의 쓰기를
허용하거나 수행했다는 뜻이 아니다.
## s6001 asset 검사 보강 기준
## 기존 PLAY_LIST read-only 재분류
`s6001` 해외지수 target은 다음 13개 파일을 요구한다.
실제 Oracle 활성 정의 23개와 연결된 2,213행은 원문의 7필드, 한국어, enabled,
ITEM_INDEX와 원본의 page 공백 제거 규칙을 보존했다.
- `Video\20201008_미국.vrv`
- `Video\20201008_독일.vrv`
- `Video\20201008_영국.vrv`
- `Video\20201008_프랑스.vrv`
- `Video\20201008_일본.vrv`
- `Video\20201008_중국.vrv`
- `Video\20201008_홍콩.vrv`
- `Video\20201008_대만.vrv`
- `Video\20201008_싱가포르.vrv`
- `Video\20201008_태국.vrv`
- `Video\20201008_필리핀.vrv`
- `Video\20201008_말레이시아.vrv`
- `Video\20201008_인도네시아.vrv`
| route | 건수 | 결과 |
|---|---:|---|
| 기존 닫힌 매퍼 | 1,250 | selection 1,250 |
| 업종 | 24 | selection 24 |
| 비교 | 99 | selection 74, 시장 identity 유실 25 차단 |
| 해외지수 candle | 61 | selection 61 |
| NXT theme | 48 | current preview empty 48 차단 |
| KRX theme | 587 | current-code 94, 나머지 identity/preview/문법/지원 경계로 차단 |
| 해외종목 | 78 | selection 78 |
| GraphE | 36 | fresh materialization 22, 값/identity 14 차단 |
| unique closed mapper 없음 | 30 | nearest-scene 추정 없이 차단 |
| 합계 | **2,213** | 통합 selection 1,603 / 명시 차단 610 |
검사 보강은 다음 조건을 만족해야 한다.
`selectionMapped`는 scene asset, PREPARE나 PGM 성공 수치가 아니다. 상세는
[`NAMED_PLAYLIST_READ_AUDIT.md`](NAMED_PLAYLIST_READ_AUDIT.md)를 따른다.
1. `.t2s` alias 결과와 asset 결과를 별도 필드로 보고한다.
2. 상대 경로만 허용하고 Cuts root 탈출을 거부한다.
3. 파일 존재, 0바이트 여부, reparse point를 검사한다.
4. 누락된 상대 경로와 영향을 받는 builder를 운영자가 알 수 있게 하되 secret이나 외부 절대 경로를 출력하지 않는다.
5. asset이 없는 현재 장비에서는 전체 migration 완료가 아니라 `외부자산 필요`로 끝나야 한다.
6. 완전 fixture, 단일 누락, 복수 누락, 0바이트, reparse fixture를 자동화한다.
## 외부자산 목록
권장 검증 명령 형태:
45개 active alias의 `.t2s`는 존재한다. 별도 종속 asset 14개만 제공되지 않았다.
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-LegacyCutCoverage.ps1 `
-CutRoot "<승인된 읽기 전용 Cuts root>"
```
- `s5006`: `Video\큐브배경.vrv`
- `s6001`:
- `Video\20201008_미국.vrv`
- `Video\20201008_독일.vrv`
- `Video\20201008_영국.vrv`
- `Video\20201008_프랑스.vrv`
- `Video\20201008_일본.vrv`
- `Video\20201008_중국.vrv`
- `Video\20201008_홍콩.vrv`
- `Video\20201008_대만.vrv`
- `Video\20201008_싱가포르.vrv`
- `Video\20201008_태국.vrv`
- `Video\20201008_필리핀.vrv`
- `Video\20201008_말레이시아.vrv`
- `Video\20201008_인도네시아.vrv`
- 기본 배경: sibling `배경\기본.vrv`
검사 보강 전의 `Missing=0``.t2s` alias만 의미하며 asset-ready 증거로 사용하면 안 된다.
이 파일들의 부재는 code/build 실패가 아니라 **외부자산 미제공**이다. 검사기는 존재,
0바이트, root escape, reparse와 manifest 중복을 검사하고 누락 경로와 영향 builder만
redacted 형태로 보고한다.
## 원본 I/O 조사에서 제외 판정한 항목
## 원본 I/O에서 의도적으로 제외한 항목
- 활성 Print/Excel/FarPoint export/import 호출은 발견되지 않았다.
- `MainForm.cs:1767-1772,1796-1805`의 CSV playlist save/load는 주석 처리돼 있다.
- `MainForm.GetThumnail`과 preview watcher/`SaveImageFile` 호출도 활성 경로가 아니다.
- `s8086`은 scene builder는 존재하지만 원본 MainForm active alias가 없어 의도적으로 unreachable이다. 이를 UI 누락으로 계산하지 않는다.
- FarPoint의 표·선택·정렬·이동 자체는 Web table/workflow로 변환할 수 있지만, 변환 UI가 존재한다는 사실만으로 DB write나 PGM 동등성이 완료되지는 않는다.
- 활성 Print/Excel/FarPoint export/import 호출은 원본에서 발견되지 않았다.
- CSV playlist save/load, thumbnail watcher와 `SaveImageFile`은 주석 또는 미호출 경로다.
- `s8086`은 builder는 있으나 원본 MainForm active alias가 없는 diagnostic route다.
- 위 dead/hidden 경로를 새 활성 기능으로 만들지 않는다.
## 기존 완료 문서 교정 후속
## 최종 증적 기록
다음 문서는 구현 존재, 자동 계약 테스트, 실데이터 read smoke, 외부 asset readiness, 실제 DB write, 실제 PGM 검증을 서로 다른 열로 분리해야 한다.
확정된 증적과 최신 전체 회귀의 기록 위치는
[`LEGACY_PARITY_VALIDATION_20260715.md`](LEGACY_PARITY_VALIDATION_20260715.md)다.
| 문서 | 교정할 표현 | 후속 교정 내용 |
|---|---|---|
| `docs/MIGRATION.md:69-74` | MainForm/UC1~UC7과 전용 화면을 포괄적으로 `완료`로 표현 | `화면·bridge 구현``운영 동등성 검증`을 분리한다. PList 한국어 복원, s6001 asset, 원본 파일 one-time import, DB write, 장면별 PGM 상태를 이 문서와 맞춘다. |
| `docs/OPERATOR_UI_PARITY.md:48-62` | 다수 화면을 `화면 통합 완료`로 표현 | 해당 표현은 구조적 UI 도달성만 뜻한다고 명시하고 DB-W/PGM 미검증을 상단 요약과 각 행에 유지한다. 완료 판정은 실제 검증 열까지 충족할 때만 사용한다. |
| `docs/OPERATOR_UI_PARITY.md:75,111-113,214-218` | PList와 비교쌍 load/save를 완료로 표현 | PList 한국어 실DB 복원과 `종목비교.dat` one-time import를 별도 미완료 항목으로 추가한다. |
| `docs/OPERATOR_UI_PARITY.md:77` | F2/F3 배경을 완료로 표현 | 별도 sibling `배경` root와 `기본.vrv` 코드/자동 검증은 반영됐다. 실제 자산이 없으므로 package/PGM 완료로 확대하지 않는다. |
| `docs/SCENE_EQUIVALENCE.md:17,98` | 전체 real-data smoke 및 s6001을 asset 경고 없이 통과로 표현 | DB→DTO→mutation smoke와 실제 asset preflight를 분리하고 s6001 국가 영상 13개를 `외부자산 필요`로 기록한다. |
| `docs/PLAYOUT_OPERATIONS.md:86,94` | s5025 환경 변수 안내와 alias-only cut 검사 | 정상 앱의 private manual store 경로와 one-time import 정책을 정확히 설명한다. cut coverage 결과에 asset 검사 상태를 추가한다. |
| `docs/PLAYOUT.md:97,348-366` | Web 배경 변경 불가 설명 및 자동 검증을 35개 완료 근거로 사용 | F2/F3 native picker의 실제 경계를 반영한다. 5001/5074 PGM 성공과 나머지 scene 자동 검증을 명확히 분리한다. |
다음 값은 추측하거나 이전 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 |
## 완료 판정에 필요한 최종 증거
1. s5006/s6001 포함 모든 reachable scene의 asset preflight 결과
2. PList 한국어 `DC_TITLE`/`LIST_TEXT` 실DB read 비교
3. 기존 `종목비교.dat`, FSell, VI 데이터의 안전한 one-time migration 결과
4. 승인된 테스트 데이터로 수행한 GraphE/PList/ThemeA/EList 및 manual file write read-back
5. Debug/Release x64 회귀와 Release MSIX package-context 실행
6. 승인된 scene·회차의 Network Monitoring 명령, callback, PGM 화면
7. timeout·OutcomeUnknown에서 재시도하지 않았다는 운영 기록
이 증거가 갖춰지기 전에는 전체 원본 UI 또는 35개 scene의 동등성 목표를 완료 처리하지 않는다.
원본 저장소는 최종 검증과 배포 이후에도 계속 읽기 전용으로 유지한다.

View File

@@ -1,101 +1,141 @@
# 원본 호환 마이그레이션 기준선
## 결정
기준일: 2026-07-15
기존 저장소와 검증된 `Core / Infrastructure / Playout`은 유지한다. 기존 루트 WinUI 앱은 비교용 프로토타입으로 보존하고, 원본 UI와 이벤트를 먼저 재현하는 `MBN_STOCK_WEBVIEW.LegacyParityApp`을 같은 솔루션에서 병행 개발한다.
## 현재 결정
WebView는 HTML 컨트롤 표시와 사용자 입력 전달만 담당한다. 검색 정책, 선택 상태, 플레이리스트 필드, 장면 alias, PageN 및 송출 상태의 권위는 C# Application 계층에 둔다.
마이그레이션의 실행 대상은 `MBN_STOCK_WEBVIEW.LegacyParityApp`이다. 이 앱은
`.NET 8 / C# / WinUI 3 / Windows App SDK 1.8 / WebView2 / Windows x64 / MSIX`
구성된 Windows 데스크톱 프로그램이다. WebView2는 앱에 포함된 로컬 HTML/CSS/JavaScript를
표시하는 컨트롤이며, 별도 웹 서버에 배포하는 웹사이트가 아니다.
## 기준 원본
원본 WinForms UI의 화면 구성과 클릭 이벤트를 먼저 동등하게 옮긴 뒤 개선한다. 기존에
검증된 `Core / Infrastructure / Playout`은 재사용하며, 원본 업무 규칙과 송출 권위는
C#에 둔다.
- 읽기 전용 저장소: `C:\Users\MD\source\repos\MBN_STOCK_N`
- 조사 시 HEAD: `309286309eaf20b58172247263f575753a860700`
- 실제 실행 컷 목록: `MBN_STOCK_N\bin\Debug\Res\종목.ini`
- 실행 컷 목록 SHA-256: `45DFB1804828F0E4A45F73D681AF0709CE5662872FAA49CFC9F7A0B940409E20`
- 실행 컷 목록은 구분행 2개를 포함한 33개 물리행이며 실제 컷은 31개다.
- 3번 컷은 고객 화면과 실행본을 따라 `1열판기본_시간외단일가`를 사용한다. 소스 `Res``시간외종가`는 기준으로 사용하지 않는다.
## 기준 원본과 보존 원칙
원본 작업 트리에는 기존 수정이 있으므로 커밋 SHA만으로 동등성을 판단하지 않는다. 원본은 계속 읽기 전용으로 취급한다.
- 읽기 전용 원본: `C:\Users\MD\source\repos\MBN_STOCK_N`
- 원본 저장소와 `MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts`는 분석·검증에만 사용한다.
- 원본 파일, 원본 설정, 실제 컷과 자산을 수정하거나 새 저장소에 복사하지 않는다.
- 벤더 DLL, K3D 라이선스, 인증서, DB 비밀번호, 실제 컷·배경 파일과 로컬 운영 설정은
Git/MSIX에 넣지 않는다.
- 실행 컷 목록은 구분행 2개를 포함한 물리행 33개와 실제 종목 컷 31개다.
- 원본 scene inventory는 35개 builder이며, MainForm에서 도달 가능한 runtime은 34개
builder와 45개 active alias다. `s8086`은 원본과 같이 무alias 진단 전용이다.
## 신규 프로젝트
## 실행 구조와 권위 경계
| 프로젝트 | 책임 |
| 계층 | 책임 |
|---|---|
| `MBN_STOCK_WEBVIEW.LegacyApplication` | 원본 MainForm/UC 상태, 이벤트 및 업무 규칙 |
| `MBN_STOCK_WEBVIEW.LegacyBridge` | 허용된 WebView 입력 intent와 표시 snapshot 계약 |
| `MBN_STOCK_WEBVIEW.LegacyParityApp` | WinUI 3 창, WebView2, DB composition root, 별도 개발 MSIX |
| `LegacyApplication.Tests` | 원본 이벤트 및 결과 DTO 동등성 테스트 |
| `LegacyBridge.Tests` | strict JSON과 Web 권한 경계 테스트 |
| `LegacyWeb.Tests` | Visual Studio에서 항상 실행되는 Web thin-client 계약 테스트 |
| `MBN_STOCK_WEBVIEW.LegacyApplication` | 원본 MainForm/UC/Form 상태, 클릭 이벤트, playlist와 업무 규칙의 C# 권위 |
| `MBN_STOCK_WEBVIEW.LegacyBridge` | WebView가 보낼 수 있는 intent의 strict JSON allowlist와 표시 snapshot 계약 |
| `MBN_STOCK_WEBVIEW.LegacyParityApp` | WinUI 3 창, 로컬 WebView2 UI, DB composition root, 송출 연결과 개발 MSIX |
| `MBN_STOCK_WEBVIEW.Core` | DTO, scene resolver/builder, PageN과 `LegacyPlayoutWorkflow` |
| `MBN_STOCK_WEBVIEW.Infrastructure` | Oracle/MariaDB와 신뢰 파일 저장소 구현 |
| `MBN_STOCK_WEBVIEW.Playout` | `IPlayoutEngine` 뒤의 K3D/Tornado2 어댑터, STA 큐와 안전 게이트 |
개발 패키지 identity는 `Wickedness.MBNStockWebView.LegacyParity`다. 운영 identity `Wickedness.MBNStockWebView`를 대체하지 않는다.
WebView는 표시와 사용자 입력 수집만 담당한다. 종목·시장 identity, 7필드 playlist 행,
scene alias, DB mutation, PageN, PREPARED/PROGRAM 상태는 Web에서 임의 생성하거나
확정할 수 없다. 모든 입력은 `LegacyBridgeProtocol`의 폐쇄형 intent로 파싱되고
`LegacyOperatorController`가 다시 검증한 뒤 실행된다.
## 첫 수직 흐름 상태
## 이관된 원본 UI와 이벤트
구현됨:
- 원본 10개 업무 탭과 탭 순서 변경
- 모든 탭에서 유지되는 종목 검색, 종목 결과, 31개 컷과 playlist 편집
- 328개 fixed action, 코스피·코스닥 업종 44개 action, 비교 대상 24개와 비교 9개 action
- UC4 테마, UC6 전문가와 UC7 거래정지의 원본 단일 클릭 동작
- UC4/UC6 목록 행과 고정 action 트리의 독립 클릭 경계
- GraphE 네 수동 재무 화면과 행 더블클릭 추가
- FSell 개인·외국인·기관 5행 입력과 VI 목록·정렬·페이지 입력
- PList/AList의 목록, 신규 생성, 저장, 불러오기, 삭제와 행 더블클릭
- ThemeA와 EList의 검색, 생성, 편집, 삭제, 종목 구성과 순서 변경
- UC5 해외업종·해외종목·해외지수
- playlist 단일/Ctrl/Shift 선택, enabled, 위/아래 이동, 선택/전체 삭제
- F2/F3 배경, Fade, NEXT/Page NEXT, TAKE OUT, 종료 확인과 자동 갱신
- 검색 버튼의 정확한 빈 문자열 no-op과 Enter 빈 검색 허용
- KOSPI → KOSDAQ → NXT KOSPI → NXT KOSDAQ 순서 및 DB 반환 순서 유지
- trim, 전체 정렬, 결과 제한 및 wildcard escape를 하지 않는 원본 호환 검색
- 바인드 파라미터를 통한 SQL injection 방지
- master snapshot을 이름으로 다시 순회하고 마지막 일치를 채택하는 `jongmokCode` 규칙
- 33개 물리 컷 행과 31개 display 번호
- 원본 `ClientSize 1905 x 1015`, `FixedSingle`, 최대화 불가 창 계약
- 일반/Ctrl/Shift/Ctrl+Shift 선택을 C# 상태로 처리
- 0.5초 이내, 동일 행, 동일 좌표 조건의 원본 더블클릭 판정
- 선택 컷 drag/drop과 물리 index 오름차순 추가
- 원본 7필드 플레이리스트 행과 중복 추가 허용
- NXT `1열판기본_현재가`만 허용하고 나머지는 원본 경고 표시
- PREPARE 직전 `LegacyPlaylistEntry`로 변환할 수 있는 31개 컷 행의 alias 매핑
FarPoint 표 자체는 Web table로 바뀌었지만 원본의 선택, 정렬, 이동, 단일/더블클릭,
저장·불러오기 이벤트 경계는 C# intent와 snapshot으로 유지한다.
아직 이관하지 않음:
## 송출 동작
- 좌측 수동 입력 GraphE 4개 모달
- 중앙 UC1~UC7과 모든 탭 이벤트
- 플레이리스트 선택/삭제/정렬/DB 저장·불러오기
- 배경, 이동평균, Fade/Update 시간 컨트롤
- PREPARE/TAKE IN/NEXT/TAKE OUT 실제 연결
- 운영 DB write와 Tornado2/PGM 검증
운영자 화면에는 원본처럼 `TAKE IN`, `NEXT`, `TAKE OUT`을 제공한다. 별도 native
`PREPARE` 버튼은 엔지니어링 진단용으로 DOM에 유지하지만 숨겨져 있다. idle 상태에서
`TAKE IN` 또는 F8을 한 번 실행하면 C#이 같은 게이트 안에서 다음 순서를 정확히 한 번씩
수행한다.
미이관 버튼은 현재 disabled 상태이며, 연결 상태는 `미연결`로 명시한다. 새 앱은 Tornado2에 연결하지 않는다.
1. 선택 playlist와 page를 C# snapshot으로 고정
2. 실제 DB/신뢰 파일에서 DTO를 다시 조회
3. `BeginTransaction` → mutation → `QueryVariables``EndTransaction`
4. `Prepare`
5. `TakeIn`
PREPARE 성공 뒤 TAKE IN이 실패하거나 결과가 불명확하면 prepared 소유권을 유지하고
자동 재시도하지 않는다. `NEXT`는 같은 scene의 다음 PageN과 다음 enabled playlist 행을
원본 조건으로 구분한다. timeout과 `OutcomeUnknown`에서는 반대 명령, cleanup 명령 또는
동일 명령을 추측해 반복하지 않는다.
기본 송출 모드는 계속 `DryRun`이다. Live는 개발 Tornado2/PGM 설정, 프로세스별 명시적
환경 승인, scene allowlist, 명령 예산을 모두 통과해야 한다. 이 안전 게이트는 개발 환경에서도
완화하지 않는다.
## scene, alias와 PageN 기준선
- 원본 scene builder source inventory: **35/35**
- 도달 가능한 active alias: **45개**, 승인된 읽기 전용 Cuts에 대응 `.t2s` 존재
- PageN: `s5074=5`, `s5077=6`, `s5088=12`개 단위
- page count: `min(20, ceil(itemCount / pageSize))`
- 빈 결과, 첫/마지막 부분 페이지, 정확한 경계, 20페이지 cap, 21페이지 거부,
같은 scene Page NEXT와 다음 playlist NEXT를 자동 테스트로 고정
장면별 세부 상태는 [`SCENE_EQUIVALENCE.md`](SCENE_EQUIVALENCE.md), 호출 순서는
[`LEGACY_PLAYOUT_ANALYSIS.md`](LEGACY_PLAYOUT_ANALYSIS.md)를 따른다.
## DB 검증 기준선
실제 Oracle/MariaDB read-only 스모크는 필수 scene selector **33/33**, 전체 query 58건을
통과했다. 이는 실제 DB → DTO → mutation 경로 검증이며 외부 영상 자산이나 PGM 출력 성공을
뜻하지 않는다.
개발 DB write 스모크는 충돌 격리된 임시 식별자로 다음 9개 시나리오의
create/save → fresh readback → delete → absence를 통과했다.
- GraphE `INPUT_PIE`, `INPUT_GROW`, `INPUT_SELL`, `INPUT_PROFIT` 4개
- PList/AList named playlist 1개
- ThemeA KRX 1개와 NXT 1개
- EList/expert 1개
스모크는 production executor를 사용하고 write 재시도를 하지 않는다. 결과가
`OutcomeUnknown`이면 cleanup이나 다음 시나리오를 실행하지 않는 규칙도 유지한다.
DB 비밀번호와 원문 데이터는 문서와 로그에 기록하지 않는다.
## 외부자산 상태
45개 active alias의 `.t2s`는 존재한다. 다만 다음 14개 종속 파일은 현재 승인 Cuts에
제공되지 않았다.
- `s5006`: `Video\큐브배경.vrv` 1개
- `s6001`: 국가별 `Video\20201008_<국가>.vrv` 13개
또한 원본 sibling `배경` 디렉터리의 `기본.vrv`도 이 장비에 없다. 이 상태는 scene/UI
코드 누락이나 마이그레이션 실패가 아니라 **Git/MSIX 밖 외부자산 미제공**이다. 자산이
없으면 해당 경로는 COM 호출 전에 fail closed하고, 다른 파일로 추정 대체하지 않는다.
## Visual Studio 실행
1. `MBN_STOCK_WEBVIEW.sln`을 연다.
2. 솔루션 구성을 `ParityDebug | x64` 선택한다.
3. `MBN_STOCK_WEBVIEW.LegacyParityApp`을 시작 프로젝트로 지정한다.
4. 실행한다.
2. `x64` 구성을 선택한다.
3. `src\MBN_STOCK_WEBVIEW.LegacyParityApp\MBN_STOCK_WEBVIEW.LegacyParityApp.csproj`
시작 프로젝트로 지정한다.
4. Debug 실행 또는 Release x64 MSIX를 생성한다.
`PrototypeDebug | x64`는 기존 루트 앱을 배포하는 비교용 구성이다. 두 앱은 서로 다른 MSIX identity와 single-instance key를 사용한다.
개발 패키지 identity는 `Wickedness.MBNStockWebView.LegacyParity`이며 기존 운영 패키지
identity를 대체하지 않는다.
기존 `Release | x64`의 자동 배포 대상은 비교용 루트 앱으로 유지한다. 호환 앱 Release는 두 패키지를 동시에 배포하지 않도록 `MBN_STOCK_WEBVIEW.LegacyParityApp` 프로젝트의 MSIX 생성 명령으로 명시적으로 만든다.
## 2026-07-15 검증 기록
## 2026-07-15 기준선 검증
- `ParityDebug | x64``Release | x64` 솔루션 빌드: 경고 0, 오류 0
- Debug/Release 각각 C# 테스트 1,980개 통과
- Core 1,383 / Infrastructure 173 / Playout 393
- LegacyApplication 17 / LegacyBridge 7 / LegacyWeb 7
- Node 기반 JavaScript syntax 및 Web 계약 테스트 7개 통과
- Release x64 개발 MSIX `0.1.0.0` 생성 및 `CN=Comtrophy` 서명 검증
- MSIX SHA-256: `D3FF9CF1F63E6BC68AA147C0ED47690FE130CF425C0276B3201F763EC5C50A4B`
- 패키지 76개 항목 중 인증서, 비밀번호, 라이선스, `.t2s`, 운영 설정 파일 0개
- 패키지의 `Web/index.html`, `styles.css`, `app.js`가 소스와 byte 단위로 일치
- 별도 패키지 전체 이름 `Wickedness.MBNStockWebView.LegacyParity_0.1.0.0_x64__qbv3jkvsn3aj0`, package family `Wickedness.MBNStockWebView.LegacyParity_qbv3jkvsn3aj0` 설치·실행 성공
- 패키지 창의 실제 WebView client bounds `1905 x 1015` 확인
- 격리 WinForms ListView trace로 최초 Shift의 native 선택 `0..현재행`과 별도 pointer 목록의 `-1..현재행`, 같은 행 Shift의 native 선택 유지와 pointer 목록 clear를 확인하고 두 상태를 분리
- 실제 DB read-only 경로로 `삼성` 검색 → `삼성출판사` 선택 → `현재가` 동일 좌표 더블클릭 → 원본 7필드 플레이리스트 행 생성 확인
- 검색 시작 직후 정상 창 닫기 경합 검증에서 10초 이내 종료 및 Windows Application 오류 이벤트 0개
이 검증에서는 DB write, PREPARE, TAKE IN, NEXT, TAKE OUT 및 Tornado2 연결을 실행하지 않았다. 화면의 송출 버튼도 계속 비활성 상태다.
## 다음 이관 순서
1. 플레이리스트 row 선택·Ctrl/Shift·삭제·전체/개별 enabled
2. 중앙 탭과 UC1부터 원본 이벤트 단위로 수직 이관
3. 수동 입력 Form과 격리 DB write/read-back
4. C# playlist authority를 기존 `LegacyPlayoutWorkflow`에 연결한 DryRun PREPARE
5. 35개 scene/PageN differential trace
6. 승인된 실제 Tornado2/PGM 검증
각 기능은 원본 입력과 신규 입력의 snapshot, DB 파라미터, 플레이리스트 7필드 및 K3D trace를 비교한 뒤 완료 처리한다.
확정된 전체 회귀, 새 Release MSIX SHA-256, package-context DryRun/Live, 개발
Tornado2 Network Monitoring과 PGM 결과는
[`LEGACY_PARITY_VALIDATION_20260715.md`](LEGACY_PARITY_VALIDATION_20260715.md)에
기록했다. 이전 패키지의 숫자나 해시는 현재 결과로 재사용하지 않았다.

View File

@@ -0,0 +1,124 @@
# LegacyParityApp 이관 검증 증적 — 2026-07-15
이 문서는 현재 `MBN_STOCK_WEBVIEW.LegacyParityApp`의 구현·통합 최종 검증 결과를
한곳에 모은다. 원본
`C:\Users\MD\source\repos\MBN_STOCK_N`은 모든 조사와 테스트에서 읽기 전용으로
취급했다.
## 제품 형태와 권위 경계
| 항목 | 결과 |
|---|---|
| 제품 형태 | `.NET 8 / C# / WinUI 3 / Windows App SDK 1.8 / WebView2 / x64 / MSIX` Windows 데스크톱 앱 |
| 웹사이트 여부 | 웹사이트가 아님. WinUI 3 프로세스가 패키지의 로컬 Web 자산을 WebView2로 표시 |
| 업무 권위 | `LegacyOperatorController`의 C# 상태와 검증된 DTO가 권위 |
| Web 경계 | `LegacyBridgeProtocol`의 strict JSON intent allowlist; Web은 DB identity, scene alias, playlist 또는 송출 상태를 확정하지 않음 |
| 벤더 경계 | COM/K3D/Tornado 구현은 `IPlayoutEngine` 뒤에 격리하고 STA 큐와 안전 게이트 유지 |
| 기본 송출 모드 | `DryRun`; Live는 별도 개발 설정, 프로세스별 승인, allowlist와 명령 예산 필요 |
| 불명확 결과 | timeout/`OutcomeUnknown`에서 자동 재시도·반대 명령·추측 cleanup 금지 |
## 원본 UI와 클릭 이벤트
| 범위 | 현재 증적 |
|---|---|
| 업무 탭 | 원본 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로 연결 |
| 해외 | UC5 해외업종·해외종목·해외지수 조회와 playlist 생성 |
| playlist | 단일/Ctrl/Shift 선택, enabled, 위/아래, 선택/전체 삭제, drag/batch 경계 |
| FarPoint 대체 | Web table로 표현하되 원본 클릭·더블클릭·정렬·이동·저장 이벤트 의미 유지 |
상세 이벤트 대조는 [`OPERATOR_UI_PARITY.md`](OPERATOR_UI_PARITY.md)를 따른다.
## 송출 흐름과 장면
| 항목 | 결과 |
|---|---|
| 원클릭 TAKE IN | idle에서 보이는 `TAKE IN`/F8 한 번으로 `PrepareAndTakeInAsync`가 page load 1회, PREPARE 1회, TAKE IN 1회를 같은 게이트에서 수행 |
| native PREPARE | 엔지니어링 진단용 control/intent는 유지하되 운영자 UI에서는 `hidden` |
| 실패 경계 | PREPARE 후 TAKE IN 실패/unknown 시 prepared state 보존, 자동 재시도 없음 |
| 장면 기준선 | 원본 builder **35/35** inventory와 mutation/runtime 자동 기준선 통과 |
| active alias | **45개** alias의 대응 `.t2s`가 승인된 읽기 전용 Cuts에 존재 |
| PageN | 5·6·12개 단위, 최대 20페이지, 경계·마지막 부분 페이지·21페이지 거부 테스트 |
| NEXT | 같은 scene의 Page NEXT와 다음 enabled playlist 행 NEXT를 원본 조건으로 구분 |
## 실제 DB 검증
| 검증 | 결과 |
|---|---|
| Oracle/MariaDB read-only | **PASS** — 필수 scene selector 33/33, 전체 query 58건 |
| GraphE write | **PASS**`INPUT_PIE`, `INPUT_GROW`, `INPUT_SELL`, `INPUT_PROFIT` 각 create, fresh readback, delete, absence |
| PList/AList write | **PASS** — named playlist 정의·항목 create/save, fresh readback, delete, absence |
| 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 |
개발 DB write는 총 9개 충돌 격리 시나리오다. production service/executor를 사용했으며
인증정보나 DB 원문을 출력하지 않았다. write 결과가 불명확한 경우에는 재시도와 cleanup을
하지 않는 구현·테스트 경계를 유지한다.
## 외부자산 판정
| 자산 | 상태 | 판정 |
|---|---|---|
| 45 active alias `.t2s` | 존재 | alias 준비 완료 |
| `s5006` `Video\큐브배경.vrv` | 1개 없음 | 외부자산 미제공; 코드 실패 아님 |
| `s6001` 국가별 영상 | 13개 없음 | 외부자산 미제공; 코드 실패 아님 |
| sibling `배경\기본.vrv` | 없음 | 외부자산 미제공; F2/F3/default background는 fail closed |
누락 자산은 Git 또는 MSIX에 임의로 넣지 않는다. 제공되면 같은 trusted-root,
0-byte, reparse, root-escape 검사를 통과한 뒤 해당 scene만 다시 검증한다.
## 개발 Tornado2와 PGM 실측
2026-07-15 개발 Tornado2 `2.7.5.49 x64 DSX10.0 S.R2`의 단일 `30001/tcp`
listener와 K3D x64 등록·라이선스를 고정한 뒤, 설치된 MSIX package-context 앱에서 다음
시퀀스를 실행했다.
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`
첫 실측과 화면 증적용 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개다.
화면 증적은 300프레임 중 72프레임이 검은 기준 화면과 달랐다. `5001` 삼성전자 화면,
`5074` page 1의 5종목, page 2의 4종목을 직접 확인했고 TAKE OUT 뒤 기준 검은 화면으로
복귀했다. 프레임 원본과 Network Monitoring 원문은 실제 컷·운영자료를 Git에 넣지 않는
정책에 따라 로컬 `artifacts/final-validation-20260715`에만 보관한다.
## 최종 전체 회귀 기록
아래 값은 모든 병렬 변경과 fixed-leaf 더블클릭 회귀 테스트가 합쳐진 뒤 다시 실행한
최종값이다.
| 항목 | 최종 결과 |
|---|---|
| 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 |
## 판정 경계
- 35/35 자동 장면 동등성은 모든 장면의 실제 PGM 출력 완료를 뜻하지 않는다.
- 33/33 DB selector 통과는 외부 영상/배경 자산 준비를 뜻하지 않는다.
- 개발 DB 9개 write 시나리오 통과는 운영 DB에 쓰기를 허용하거나 수행했다는 뜻이 아니다.
- 개발 Tornado2/PGM은 안전 게이트를 유지한 별도 통합 검증 대상이다.
- 원본 저장소, 실제 Cuts, 라이선스와 인증정보는 계속 읽기 전용 또는 Git 외부로 유지한다.

View File

@@ -1,288 +1,175 @@
# 운영자 UI 동등성 인벤토리
## 문서 목적과 판정 기준
기준일: 2026-07-15
이 문서는 원본 WinForms 운영 화면의 사용자 동작을 새 WinUI 3/WebView 구현에 1:1로 연결한다. 장면 builder가 존재하는 것, 네이티브 bridge와 workflow 모듈이 존재하는 것, 실제 Web 화면에서 운영자가 사용할 수 있는 것은 서로 다른 완료 조건으로 취급한다.
## 제품과 판정 기준
상태 표기는 다음과 같다. 아래 기존 행의 `화면 통합 완료` 표현도 `화면/계약 구현`과 같은 구조적 도달성만 뜻하며, 운영 동등성 완료로 읽지 않는다.
현재 이관 앱은 웹사이트가 아니라 `.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에 없는 필드·값·명령을 거부한다.
이 문서의 판정은 다음을 구분한다.
| 판정 | 의미 |
|---|---|
| 화면/계약 구현 | 현재 `Web/index.html``Web/app.js`에서 검색·선택·플레이리스트 추가 또는 저장 동작에 도달하며 native bridge·workflow 계약이 있다. |
| 화면/계약 구현 / 실환경 검증 대기 | 화면, native bridge, workflow와 자동 테스트까지 연결됐지만 운영 DB write, 기존 데이터 identity/문자 복원 또는 해당 경로의 실제 PGM 확인을 수행하지 않았다. |
| 운영 동등성 일부 검증 | 승인된 제한 데이터·scene·회차만 실제 DB/PGM으로 확인했다. 확인하지 않은 범위로 확대하지 않는다. |
| 운영 동등성 완료 | 원본과 같은 입력·저장·재조회·identity·PGM 결과를 해당 기능 범위 전체에서 확인한 상태다. 현재 전체 UI에 이 판정을 적용하지 않는다. |
| UI/이벤트 이관 | 원본 컨트롤과 클릭 이벤트가 실제 LegacyParityApp 화면 → strict bridge → C# workflow로 연결됨 |
| 자동 동등성 | 원본 inventory·DTO·mutation·호출 순서·경계값을 자동 테스트로 비교함 |
| 개발 DB 통합 | 실제 개발 Oracle/MariaDB에서 create/readback/delete/absence를 확인함 |
| 개발 PGM 통합 | 개발 Tornado2 Network Monitoring과 PGM 화면을 함께 확인함 |
| 외부자산 필요 | 코드 경로는 있으나 Git/MSIX 밖의 실제 영상·배경이 제공되지 않음 |
`DB-W`는 운영 데이터베이스에 실제 쓰기를 수행해 확인했는지를 뜻한다. `PGM`은 실제 Tornado2 PGM 출력으로 해당 UI 경로를 확인했는지를 뜻한다. fake executor, DryRun, SQL 계약 테스트는 자동 테스트 근거이지 실제 DB-W 또는 PGM 검증으로 계산하지 않는다.
## 원본 보존
기능별 미적용·적용 중·외부자산 필요 판정과 완료에 필요한 증거는 [`LEGACY_FEATURE_AUDIT.md`](LEGACY_FEATURE_AUDIT.md)를 따른다.
- 원본 `C:\Users\MD\source\repos\MBN_STOCK_N`은 읽기 전용으로만 분석했다.
- 원본 `Cuts`도 읽기 전용 검증에만 사용하며 수정·복사하지 않는다.
- 비밀번호, 라이선스, 벤더 DLL, 인증서, 실제 컷·영상·배경과 로컬 설정은 Git에 넣지 않는다.
## 원본 보존 상태
## 원본 실행 action inventory
- 원본 저장소는 읽기 전용으로만 분석했고 파일을 수정하지 않다.
- 분석 시작 시 원본에 이미 존재하던 dirty 파일은 `MBN_STOCK_N/MainForm.cs`, `MBN_STOCK_N/RES/MmoneyCoder.ini` 두 개였다. 두 파일의 기존 변경을 그대로 보존했다.
- 이 문서에는 비밀번호, 실제 방송 데이터, 자산·경로 해시를 기록하지 않는다.
동적 테마·전문가 DB 행과 전용 편집기의 데이터 행은 아래 고정 수량에 중복해 더하지 않다.
## 412개 실행 action의 고정 분해
| 집합 | 원본 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 기반 활성화·순서 변경 | 이관 |
412는 다음 네 집합의 합이다. 동적 테마·전문가 목록과 GraphE 편집 행 수는 이 숫자에 중복해 더하지 않는다.
328개 fixed action 중 수동 전제 6개는 임의 DTO로 만들지 않는다. 개인·외국인·기관
순매도 3개는 FSell의 신뢰된 5행 입력으로, 5단·6종목·12종목 트리의 VI placeholder 3개는
원본 VIList가 만든 `s5074/5074` 5개 단위 PageN으로만 활성화한다.
| 집합 | 원본 UI | 수량 | 새 구현 | 자동 근거 | 상태 |
|---|---|---:|---|---|---|
| 고정 action | `MainForm`의 해외·환율·지수 탭과 `Control/UC1.cs` | 328 | `Web/legacy-fixed-workflow.js` | `tests/Web/legacy-fixed-workflow.test.cjs`가 328개, 직접 생성 가능한 322개, 수동 전제 6개를 고정한다. | 화면 통합 완료. 수동 6개는 아래 대체 경계를 통과해야 한다. |
| 업종 action | `Control/UC1.cs` + `Control/UC2.cs`의 코스피·코스닥 | 44 | `Web/industry-workflow.js`의 시장별 22개 | `tests/Web/industry-workflow.test.cjs` | 화면 통합 완료 |
| 종목 action | `MainForm` 좌측 종목 검색·컷 목록 | 31 | `Web/operator-workflow.js` | `tests/Web/operator-workflow.test.cjs`가 실행 자산 순서의 31개를 고정한다. | 화면 통합 완료 |
| 비교 action | `Control/UC1.cs` + `Control/UC3.cs` | 9 | `Web/comparison-workflow.js` | `tests/Web/comparison-workflow.test.cjs`가 24개 비교 대상과 9개 action을 별도로 고정한다. | 화면 통합 완료 |
| 합계 | | **412** | | | |
## 화면 단위 현황
고정 action 328개 안의 수동 전제 6개는 일반 고정 DTO로 임의 생성하지 않는다.
| 원본 고정 항목 | 수량 | 원본 전용 폼 | 새 대체 경계 | 관계 |
|---|---:|---|---|---|
| 개인·외국인·기관 순매도 상위(수동) | 3 | `Form/FSell.cs` | `MainWindow.ManualLists.cs`, `Web/manual-lists-workflow.js`, 신뢰된 s5025 파일 저장소 | 각 audience의 검증된 5행 입력이 있어야 `s5025/5025` 플레이리스트 항목을 만든다. |
| 5단·6종목·12종목 섹션의 VI 발동(수동) placeholder | 3 | `Form/VIList.cs` | `MainWindow.ManualLists.cs`, `Web/manual-lists-workflow.js`, 신뢰된 VI 저장소 | 세 placeholder 모두 원본 VIList가 만든 하나의 `s5074/5074` 5행 페이지 결과로 대체된다. 6·12행 장면으로 바꾸지 않는다. |
현재 이 수동 6개는 FSell/VIList 대체 화면까지 통합됐다. 고정 카탈로그에서 임의 DTO로 직접 활성화하지 않고, 전용 입력 화면이 저장 후 재조회한 신뢰 데이터와 버전을 공급한 뒤에만 플레이리스트에 넣는다.
## 화면 단위 완료 요약
| 원본 화면 | 핵심 역할 | 화면/계약 구현 상태 | 운영 동등성 DB-W | 운영 동등성 PGM |
| 원본 화면 | 이관된 핵심 동작 | 현재 구현 | 개발 DB/파일 검증 | 개발 PGM/자산 경계 |
|---|---|---|---|---|
| MainForm | 종목 검색, 31컷, playlist, 송출·배경·저장 | 화면/계약 구현. sibling `배경` root 코드는 적용됐고 실제 외부 배경 자산·package 검증 대기 | 없음/해당 없음 | 일부: 승인된 `5001`, `5074`, TAKE OUT |
| UC1 | 328 fixed, 업종·비교 action 트리 | 412 action과 수동 6개 전용 편집 화면/계약 구현 | 해당 없음 | 일부: generic `5074`; 전체 action 미검증 |
| UC2 | 업종 목록과 두 업종 선택 | 화면/계약 구현 | 해당 없음 | 미검증 |
| UC3 | 비교 대상·저장 비교쌍·9 action | 화면/계약과 strict `종목비교.dat` one-time importer 구현 | importer는 read-only이며 DB-W 없음 | signed package에서 8쌍 1회 실행·재시작 영속성 통과; PGM 미검증 |
| UC4 | 테마 검색·preview·5/6/12행 항목 | 조회/선택 및 ThemeA CRUD 화면/identity 계약 구현 | CRUD 미실행; 실제 KRX/NXT identity 미검증 | 미검증 |
| UC5 | 해외업종·해외종목·해외지수 | 화면/계약 구현 | 해당 없음 | 미검증; `s6001` 국가 영상 13개 외부자산 필요 |
| UC6 | 전문가 조회·추천 preview·5행 항목 | 조회/선택 및 EList/추천 CRUD 화면/identity 계약 구현 | CRUD 미실행; 실제 identity 미검증 | 미검증 |
| UC7 | 거래정지 검색·현재/120일 캔들 | 화면/계약 구현 | 해당 없음 | 미검증 |
| GraphE | 4개 수동 재무 CRUD와 playlist 추가 | 화면/계약 구현 / 실환경 검증 대기 | 미실행 | 미검증 |
| FSell | 개인·외국인·기관 수동 순매도 5행 | 화면/계약, 원본 importer와 production parser 구현 | 운영 데이터 write 미실행 | 현재 3 audience×5행 read-only 검증; 기존 파일 불변 |
| VIList | 수동 VI 목록·순서·5개 단위 page | 화면/계약, 원본 importer와 versioned production parser 구현 | 운영 데이터 write 미실행 | 현재 9항목·2 page read-only 검증; 기존 파일 불변 |
| PList | 이름 있는 playlist 목록·저장·불러오기·삭제 | 화면/계약과 한국어 legacy/KRX/NXT/해외종목/manual restore 구현 | 운영 DB write 미실행 | 2,213행 무손상, 통합 selection 1,603/차단 610; scene/PGM 별도 |
| AList | 프로그램 코드·이름 생성 | PList 화면/계약에 통합 | 운영 DB write 미실행 | 해당 없음 |
| ThemeA | KRX/NXT 테마 구성 CRUD | 화면/identity 계약 구현 / 실환경 검증 대기 | 미실행 | 미검증 |
| EList | 전문가 생성과 추천 구성 CRUD | 화면/identity 계약 구현 / 실환경 검증 대기 | 미실행 | 미검증 |
| 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` | 개발 DB KRX/NXT 각 1개 PASS | scene PGM 별도 |
| EList | 전문가 생성·편집·삭제, 추천 종목·금액·순서 | `LegacyOperatorCatalogWorkflow` | 개발 DB expert 1개 PASS | scene PGM 별도 |
## MainForm 매핑
UC4/UC6의 결과 행은 고정 action 트리의 click handler에 버블링하지 않는다. UC4 테마,
UC6 전문가와 UC7 거래정지는 원본처럼 **단일 클릭**으로 선택/playlist 동작을 수행하고,
fixed/industry/comparison 트리의 원본 double-click과 독립적으로 처리한다.
원본 파일은 `MainForm.cs``MainForm.Designer.cs`다.
## MainForm 이벤트 매핑
| 원본 이벤트·컨트롤 | 원본 동작과 입력/저장 경계 | 새 구현 파일·bridge/workflow | 현재 상태 | 자동 테스트 근거 | DB-W / PGM |
|---|---|---|---|---|---|
| `Form1_Load`, `tabControl1_SelectedIndexChanged` | 실행 자산을 읽고 UC1~UC7을 탭에 배치하며 Tornado에 연결한다. | `MainWindow.xaml.cs`, `MainWindow.Playout.cs`, `Web/index.html`, `Web/app.js` | 화면 통합 완료 | `LegacyWebWorkflowIntegrationTests.cs`, `PlayoutBridgeProtocolTests.cs`, Web 전체 회귀 | DB-W 해당 없음 / PGM 일부 완료 |
| `tab_MouseDown`/`tab_MouseMove`/`tab_DragOver`, `swapTabPages` | 원본 10개 업무 탭의 표시 순서를 drag 중 identity 단위로 교환한다. | `Web/market-nav-reorder.js`; dashboard 고정; `data-market` 기반 drag와 Alt+위/아래; 세션 한정 | 코드 적용 / signed package 검증 대기. active view·DB 요청·playlist 상태를 변경하지 않음 | 전용 10/10, 전체 Web 353/353, Debug/Release x64 build | DB-W·PGM 해당 없음 |
| `button9_Click`, `txtSearch_KeyPress`, `searchStock`, `jongmokCode` | 코스피·코스닥·NXT 두 시장씩 검색하고 이름, 코드, 시장을 선택한다. | `src/MBN_STOCK_WEBVIEW.Core/Data/StockSearch.cs`; native `search-stocks`; `Web/operator-workflow.js` | 화면 통합 완료 | `LegacyStockSearchServiceTests.cs`, `operator-workflow.test.cjs` | DB-W 없음 / `5001` 경로 일부 PGM 완료 |
| `listView1_*`, `GridView1_*`, `SetPlayList`, `PlayList_DragDrop` | 실행 자산의 종목 컷을 선택·드래그해 enabled, 시장, 종목, 종류, 세부사항, page, 종목코드 행을 만든다. | `Web/operator-workflow.js`, `Web/app.js`의 종목 workflow와 표준 playlist entry | 화면 통합 완료 | `operator-workflow.test.cjs`, `candle-options-workflow.test.cjs` | DB-W 없음 / 전체 31개 PGM 미검증 |
| `b1_Click` (`주요매출 구성`, `성장성 지표`, `매출액`, `영업이익`) | `GraphE` 네 모드를 연다. | `MainWindow.ManualFinancial.cs`, `Web/manual-financial-workflow.js`, `Web/manual-financial-ui.js` | 화면 통합 완료 / 실환경 검증 대기 | GraphE 표 참조 | DB-W 없음 / PGM 없음 |
| `PlayList_CellClick`, `PlayList_KeyPress`, `checkBox1_CheckedChanged`, `btnup_Click`, `btndown_Click`, `btndelete_Click`, `btntdelete_Click` | 단일·Ctrl·Shift 선택, Space/전체 송출 체크, 순서 이동, 선택·전체 삭제. 송출 중 이동·전체 삭제를 막는다. | `Web/app.js`의 playlist 상태, snapshot lock, `Web/playout-safety.js` | 화면 통합 완료 | `playout-safety.test.cjs`, named/각 workflow의 trusted restore 테스트 | DB-W 없음 / PGM 제어 일부 완료 |
| `button5_Click`, `btnload_Click` | `PList`를 열어 이름 있는 DB 플레이리스트를 저장·불러온다. | `MainWindow.NamedPlaylists.cs`, `src/MBN_STOCK_WEBVIEW.Core/Data/NamedPlaylistPersistence.cs`, `src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OracleNamedPlaylistMutationExecutor.cs`, `Web/named-playlist-workflow.js` | 화면/계약 구현. 기존 한국어 행과 manual 항목의 실DB fresh restore는 적용 중 | named playlist Core/Infrastructure/Web/bridge 테스트 | DB-W 없음 / 복원 항목별 PGM 없음 |
| `btnPrepare_Click`, `btnTake_Click`/F8, `btnNext_Click`, `btnTakeout_Click`/Esc | PREPARE, TAKE IN, 같은 장면 page NEXT 또는 다음 행 NEXT, StopAll 기반 TAKE OUT. | `MainWindow.Playout.cs`; `IPlayoutEngine`; native `playout-command`, `request-playlist-page-plans`; `Web/playout-safety.js` | 화면 통합 완료 | playout/Core/PageN/PGM sequence 테스트와 `playout-safety.test.cjs` | DB-W 해당 없음 / 승인된 `5001`, `5074` page 1·2 및 TAKE OUT만 PGM 확인 |
| `btnback_Click`/F2, `ckback_CheckedChanged`/F3 | 허용 배경 파일 선택, 배경 없음 토글, 송출 중 변경 금지. | `MainWindow.Background.cs`; 별도 `LegacyBackgroundDirectory`; opened-handle/hardlink/reparse 검증; native `request-background-status`, `choose-background`, `toggle-background`; Web 배경 컨트롤 | sibling `배경` root와 `기본.vrv` 코드 적용. 현재 원본 외부 폴더가 없어 fail closed이며 package/PGM 검증 대기 | 별도 root 내부/escape/reparse/hardlink/opened-handle/누락 fixture, Core·Playout Debug/Release 전부 통과, 앱 Debug/Release build | DB-W 해당 없음 / 기능별 PGM 미검증 |
| `timer1_Tick`, `timer2_Tick`, `timer3_Tick`, `MainForm_FormClosing`, `MainForm_Resize`, keyboard handlers | 자동 갱신, 상태 표시, 종료 정리, 단축키를 담당한다. | playout refresh scheduler, cancellation/lifetime hooks, Web 상태·단축키 | 화면 통합 완료 | refresh epoch/scheduler, playout safety, bridge 테스트 | DB-W 해당 없음 / 일부 PGM 완료 |
실제 PGM 완료는 이미 승인된 제한 회차의 `5001``5074` 흐름만 뜻한다. 412개 전체나 아래 편집 화면의 실제 PGM 동등성을 뜻하지 않는다.
## UC1~UC7 매핑
### UC1 — 해외·환율·지수·업종·비교 action 트리
원본: `Control/UC1.cs`, `Control/UC1.Designer.cs`
| 원본 이벤트·버튼 | 원본 경계 | 새 구현 | 상태·검증 |
|---|---|---|---|
| `UC1_Load`, `INI_parser` | 탭별 실행 자산을 읽어 category/leaf TreeView를 구성한다. | `legacy-fixed-workflow.js`, `industry-workflow.js`, `comparison-workflow.js`의 폐쇄형 inventory | 화면 통합 완료. 각 Web 테스트가 수량·중복·매핑을 고정한다. |
| `chE_CheckedChanged`, `treeView_BeforeSelect`, `treeView_BeforeExpand/BeforeCollapse`, `treeView_DrawNode`, `treeView_MouseDown` | 전체 expand/collapse와 트리 표시를 제어한다. | Web category/section 렌더링과 펼침 상태 | 화면 통합 완료. 시각 상호작용이며 DB-W/PGM 대상 아님. |
| `treeView_DoubleClick` | category 전체 또는 leaf를 playlist에 넣고 5·6·12개 PageN, NXT 제한, 업종 쌍, 비교 쌍을 계산한다. FSell·VI 수동 항목이면 전용 폼으로 분기한다. | 고정 328, 업종 44, 비교 9 workflow의 `create*PlaylistEntry`; page plan bridge; 수동 6개는 `manual-lists-workflow.js``manual-lists-ui.js`로 분리 | 전체 화면 통합 완료. 실제 DB-W 없음, 전체 PGM 미검증. |
| `treeView_AfterSelect`, `btnswap_Click` | 원본에서 실질 동작이 없거나 주석 처리된 이벤트다. | 별도 기능 없음 | 의도적으로 추가 동작을 만들지 않는다. |
### UC2 — 코스피·코스닥 업종 선택과 쌍
원본: `Control/UC2.cs`, `Control/UC2.Designer.cs`
| 원본 이벤트·버튼 | 원본 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `UC2_Load` | 코스피/코스닥 업종 이름·코드를 DB에서 읽는다. | `src/MBN_STOCK_WEBVIEW.Core/Data/IndustrySelection.cs`; native `request-industries` | 화면 통합 완료. `LegacyIndustrySelectionServiceTests.cs`, `industry-workflow.test.cjs`. |
| `listbox_DoubleClick`, `btnswap_Click` | 선택 업종을 두 슬롯에 회전 입력하고 순서를 바꾼다. | `Web/industry-workflow.js`의 selected/primary/secondary 입력 | 화면 통합 완료. 시장 혼합과 빈 슬롯을 fail closed 한다. |
| UC1 업종 leaf 실행 | 단일, 2열, 수익률 5종, 캔들 12종, 시장 전체 3종으로 시장별 22개를 만든다. | `industry-workflow.js` | 44개 화면 통합 완료. 실제 DB-W 없음 / 업종별 PGM 없음. |
### UC3 — 비교 대상·저장 비교쌍
원본: `Control/UC3.cs`, `Control/UC3.Designer.cs`
| 원본 이벤트·버튼 | 원본 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `UC3_Load`, `Data_Load` | 24개 고정 비교 대상을 읽고 로컬 `종목비교.dat`의 저장쌍을 불러온다. | `Web/comparison-workflow.js`; 24개 typed target; schema-versioned pair list; `LegacyComparisonPairImport`의 고정 read-only source와 strict CP949 9필드 importer | signed package에서 실제 DB identity를 다시 읽어 8쌍을 한 번 가져왔고, 정상 재시작 뒤 private marker·순서를 확인했다. 원본을 계속 참조하지 않는다. |
| `btnsearch_Click`, `listBox_DoubleClick`, `listBox1_DoubleClick`, `btnsearch2_Click`, `FpSpread2_CellDoubleClick` | 국내 종목, 24개 시장 대상, 해외 종목을 두 슬롯에 회전 입력한다. | native `search-stocks`, `search-world-stocks`; comparison target normalizer | 화면 통합 완료. `comparison-workflow.test.cjs`가 회전·시장·해외 identity를 검증한다. |
| `btnswap_Click`, `btnAdd_Click`, `btn1/2/3/4_Click`, `File_Save` | 쌍 교환, 추가, 선택/전체 삭제, 위/아래 이동 후 저장한다. | comparison pair append/delete/delete-all/reorder와 Web storage; explicit legacy import button | 화면/계약 구현. 기존 CP949 파일은 exact 9필드·fresh DB identity·중복·500쌍 경계를 모두 통과해야 전체가 원자적으로 저장된다. 실제 8쌍 import와 재시작 marker를 확인했다. |
| UC1 비교 leaf 실행 | 현재/예상 2열, 비교 캔들/라인, 5일·1/3/6/12개월 수익률의 9개 action | `comparison-workflow.js` | 9개 화면 통합 완료. 실제 DB-W 없음 / 비교 PGM 없음. |
### UC4 — 테마 검색·미리보기·송출 선택
원본: `Control/UC4.cs`, `Control/UC4.Designer.cs`
| 원본 이벤트·버튼 | 원본 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `btn1_Click`, `GetThemeByKeyword` | KRX Oracle과 NXT 저장소에서 테마명·코드를 검색한다. | `src/MBN_STOCK_WEBVIEW.Core/Data/ThemeSelection.cs`; native `search-themes`; `Web/theme-workflow.js` | 검색 화면 통합 완료. `LegacyThemeSelectionServiceTests.cs`, `theme-workflow.test.cjs`. |
| `FpSpread1_CellClick` | 선택 테마의 구성 종목과 입력 순서를 DB에서 읽고 중지 종목을 제외한다. | native `request-theme-preview`와 typed preview | 화면 통합 완료. 중복 key·시장 불일치·truncated preview를 거부한다. |
| `btnnow_Click`, `btnpre_Click`, `btnnow_6_Click`, `btnnow_12_Click`, double-click | 5단 현재/예상, 6종목 현재, 12종목 현재를 입력순·상승률·하락률로 만들고 5/6/12 page 수를 계산한다. NXT 예상은 차단한다. | `theme-workflow.js`의 compatibility, page preview, playlist entry | 화면 통합 완료. 실제 DB-W 없음 / 테마 PGM 없음. |
| `btn2_Click`, `btn3_Click`, `btn4_Click` | ThemeA 추가·편집 폼을 열거나 선택 테마와 구성 종목을 삭제한다. | `MainWindow.OperatorCatalogs.cs`, `Web/operator-catalog-workflow.js`, `Web/operator-catalog-ui.js` | ThemeA CRUD 화면 통합 완료. 실제 DB-W/PGM 없음. |
### UC5 — 해외업종·해외종목·해외지수
원본: `Control/UC5.cs`, `Control/UC5.Designer.cs`
| 원본 이벤트·버튼 | 원본 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `UC5_Load` | 고정 해외지수 목록을 표시한다. | `Web/overseas-workflow.js`의 고정 지수 asset | 화면 통합 완료. |
| `btnsearch1_Click`, `upjong_search` | 미국 해외업종의 입력명·표시명·symbol을 조회한다. | `src/MBN_STOCK_WEBVIEW.Core/Data/OverseasIndustryIndexSearch.cs`; native `search-overseas-industries` | 화면 통합 완료. `LegacyOverseasIndustryIndexSearchServiceTests.cs`. |
| `btnsearch2_Click`, `jongmok_search` | US/TW 해외종목 이름·symbol을 조회한다. | `src/MBN_STOCK_WEBVIEW.Core/Data/WorldStockSearch.cs`; native `search-world-stocks` | 화면 통합 완료. `LegacyWorldStockSearchServiceTests.cs`. |
| `btn1_Click`, `Account_Graph` | 업종/종목 현재 1열과 지수·종목 5/20/60/120/240일 캔들을 playlist에 넣는다. | `Web/overseas-workflow.js` | 화면 통합 완료. `overseas-workflow.test.cjs`; 실제 DB-W 없음 / 해외 PGM 없음. |
### UC6 — 전문가 목록·추천 종목·송출 선택
원본: `Control/UC6.cs`, `Control/UC6.Designer.cs`
| 원본 이벤트·버튼 | 원본 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `UC6_Load`, `Data_Load`, `FpSpread1_CellClick` | `EXPERT_LIST`를 읽고 선택 전문가의 `RECOMMEND_LIST`를 PLAYINDEX 순서로 미리 본다. | `src/MBN_STOCK_WEBVIEW.Core/Data/ExpertSelection.cs`; native `search-experts`, `request-expert-preview`; `Web/expert-workflow.js` | 검색·선택 화면 통합 완료. `LegacyExpertSelectionServiceTests.cs`, `expert-workflow.test.cjs`. |
| `btlistadd_Click` | 선택 전문가를 5행 page, 최대 20 page의 전문가 추천 항목으로 만든다. | `expert-workflow.js`의 native-normalized preview와 playlist entry | 화면 통합 완료. 실제 DB-W 없음 / 전문가 PGM 없음. |
| `btnAdd_Click` | EList 신규 전문가 폼을 연다. | operator catalog expert create와 `operator-catalog-ui.js` | EList 신규 UI 통합 완료. |
| `btnDelete_Click`, `btndel_Click`, `btnsave_Click`, 종목 검색·double-click | 전문가 삭제, 추천 종목 편집·삭제·재정렬·저장. 원본은 전문가/추천 행을 삭제 후 재삽입한다. | `MainWindow.OperatorCatalogs.cs`, `src/MBN_STOCK_WEBVIEW.Core/Data/ExpertCatalogPersistence.cs`, `src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OperatorCatalogMutationExecutor.cs`, `Web/operator-catalog-workflow.js`, `Web/operator-catalog-ui.js` | EList/전문가 CRUD 화면 통합 완료. 실제 DB-W/PGM 없음. |
### UC7 — 거래정지 종목
원본: `Control/UC7.cs`, `Control/UC7.Designer.cs`
| 원본 이벤트·버튼 | 원본 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `btnsearch_Click`, `txts_KeyPress`, `search` | 거래정지 코스피와 코스닥 종목을 빈 검색 또는 부분 검색으로 합친다. | `src/MBN_STOCK_WEBVIEW.Core/Data/TradingHaltSelection.cs`; native `search-trading-halts`; `Web/trading-halt-workflow.js` | 화면 통합 완료. `LegacyTradingHaltSelectionServiceTests.cs`. |
| `FpSpread1_CellClick` | 종목명 열 정렬 | Web 정규화 결과의 결정적 정렬 | 화면 통합 완료. |
| `btn4_Click` (`현재가`, `120일 캔들`) | 시장·종목 identity로 현재 1열 또는 거래정지 120일 캔들 항목을 만든다. | `trading-halt-workflow.js` | 화면 통합 완료. `trading-halt-workflow.test.cjs`; 실제 DB-W 없음 / PGM 없음. |
## 전용 Form 매핑
### GraphE — 네 가지 수동 재무 편집기
원본: `Form/GraphE.cs`, `Form/GraphE.Designer.cs`
| 원본 이벤트·버튼 | 원본 입력·저장 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `GraphE_Load`, `Data_Init`, `FpSpread1_CellClick`, `Data_Load` | 네 테이블 전체/검색 목록을 읽고 선택 행을 편집 DTO로 분해한다. | `src/MBN_STOCK_WEBVIEW.Core/Data/ManualFinancialScreens.cs`; native `request-manual-financial-list`, `request-manual-financial-load`; `Web/manual-financial-ui.js` | 화면 통합 완료 / 실환경 검증 대기 |
| 종목 검색, `listBox_DoubleClick`, `upSearch_Click`, `downSearch_Click` | 정확한 종목을 선택하고 테이블 내 이름을 위/아래로 찾는다. | native stock master 재검증과 manual financial list/search | 화면 통합 완료. 이름만으로 시장·코드를 추정하지 않는다. |
| `btadd_Click` | 기존 이름이면 update, 아니면 insert. `INPUT_PIE`, `INPUT_GROW`, `INPUT_SELL`, `INPUT_PROFIT`의 폐쇄형 필드를 저장한다. | native `create-manual-financial-record`, `update-manual-financial-record`; `src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OracleManualFinancialMutationExecutor.cs` | 화면 통합 완료. 단일 트랜잭션, optimistic `rowVersion`, no retry, OutcomeUnknown quarantine 구현. 실제 DB-W 없음. |
| `btndel_Click`, `btnall_Click` | 선택 이름 삭제 또는 테이블 전체 삭제 확인 | native `delete-manual-financial-record`, `delete-all-manual-financial-records` | 화면 통합 완료. 실제 DB-W 없음. |
| `btnAdd_Click`, row double-click, `Select_Item` | DB에 존재하는 행을 정확한 시장/종목과 결합해 1 page playlist 항목으로 추가한다. | native `request-manual-financial-selection`; `Web/manual-financial-workflow.js` | 화면 통합 완료. 저장 복원본은 fresh load + rowVersion/record 일치 + stock 재검증 + native selection 전까지 PREPARE 불가. PGM 없음. |
화면별 폐쇄형 매핑은 다음과 같다.
| 원본 모드 | 테이블 | 입력 | builder/code |
|---|---|---|---|
| 주요매출 구성 | `INPUT_PIE` | 종목, 기준일, 최대 5개 구성명·비율 | `s5076/5076` |
| 성장성 지표 | `INPUT_GROW` | 매출·영업이익·순자산·순이익 증가율 각 4개와 분기 4개 | `s5079/5079` |
| 매출액 | `INPUT_SELL` | 분기·금액 6쌍 | `s5080/5080` |
| 영업이익 | `INPUT_PROFIT` | 분기·금액 6쌍 | `s5081/5081` |
자동 근거는 `LegacyManualFinancialScreenServiceTests.cs`, `OracleManualFinancialMutationExecutorTests.cs`, `manual-financial-workflow.test.cjs`, `manual-financial-bridge-integration.test.cjs`다.
### FSell — 수동 순매도 3종
원본: `Form/FSell.cs`, `Form/FSell.Designer.cs`
| 원본 이벤트·버튼 | 원본 입력·저장 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `FSell_Load`, `data_load`, `ReadListFile` | 개인·외국인·기관별 로컬 파일에서 5행, 행마다 좌/우 종목·금액을 읽는다. | `src/MBN_STOCK_WEBVIEW.Infrastructure/Playout/S5025TrustedManualFileDataSource.cs`의 store/data source; native `request-manual-net-sell-data`; `Web/manual-lists-ui.js` | 화면 통합 완료 / 실환경 검증 대기 |
| `btnok_Click`, `WriteListFile` | 정확히 5행을 저장하고 수동 순매도 항목 생성을 허용한다. | native `save-manual-net-sell-data`; `Web/manual-lists-workflow.js` | 화면 통합 완료. 검증된 versioned 데이터만 `s5025/5025`로 변환한다. 실제 파일 write는 자동 테스트 임시 디렉터리에서만 확인, 운영 데이터 write 없음. |
| `btncn_Click` | 취소 시 placeholder 행을 유지하지 않는다. | workflow가 저장 성공·신뢰 데이터 없이는 항목을 만들지 않는다. | 화면 통합 완료. PGM 없음. |
자동 근거는 `TrustedManualSceneDataLoader5025Tests.cs`, `S5025TrustedManualFileDataSourceTests.cs`, `S5025TrustedManualFileStoreTests.cs`, `manual-lists-workflow.test.cjs`다.
### VIList — 수동 VI 목록
원본: `Form/VIList.cs`, `Form/VIList.Designer.cs`
| 원본 이벤트·버튼 | 원본 입력·저장 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `VILIST_Load` | 로컬 VI 목록을 읽는다. | `src/MBN_STOCK_WEBVIEW.Infrastructure/Playout/ViTrustedManualFileStore.cs`; native `request-vi-manual-list`; `Web/manual-lists-ui.js` | 화면 통합 완료 / 실환경 검증 대기 |
| 종목 검색, `listBox_DoubleClick` | 종목 master에서 코스피/코스닥 prefix와 종목명을 추가한다. | native `search-stocks`, `manual-lists-workflow.js`의 strict VI item | 화면 통합 완료. |
| `btn1/2/3/4_Click` | 선택/전체 삭제와 위/아래 이동 | manual lists draft operations | 화면 통합 완료. |
| `btnok_Click` | 목록을 저장하고 종목명을 comma로 결합해 항상 `5단 표그래프 / VI 발동(수동)`으로 추가하며 page를 5개 단위로 계산한다. | native `save-vi-manual-list`; trusted `s5074/5074`, 5개 단위 page entry | 화면 통합 완료. 자동 테스트는 `ViTrustedManualFileStoreTests.cs`, `manual-lists-workflow.test.cjs`, fixed workflow의 VI placeholder 테스트. 운영 write 없음. |
| `btncancel_Click` | 저장·추가 없이 닫는다. | workflow 취소 시 mutation 없음 | 화면 통합 완료. 수동 VI dataset의 PGM 없음. 기존 generic `5074` PGM 검증을 VI 입력 검증으로 간주하지 않는다. |
### PList + AList — 이름 있는 DB 플레이리스트
원본: `Form/PList.cs`, `Form/AList.cs`와 각 Designer
| 원본 이벤트·버튼 | 원본 입력·저장 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `PList_Load`, `Get_FileName_Folder` | `DC_LIST`의 프로그램 제목·코드를 조회한다. | native `request-named-playlist-list`; `named-playlist-workflow.js` | 화면 통합 완료 |
| `AList_Load`, `GetNextCode`, `btnok_Click` | 다음 8자리 코드와 프로그램명을 생성해 `DC_LIST`에 추가한다. | native `create-named-playlist` | 화면 통합 완료. parameterized transaction 사용. |
| `PList.data_save`, double-click/확인 | 기존 `PLAY_LIST`를 지운 뒤 enabled와 6개 표시 필드를 `LIST_TEXT` 및 ITEM_INDEX 순서로 저장한다. | native `replace-named-playlist`; trusted entry serialization | 화면/계약 구현. raw·untrusted entry는 저장 또는 PREPARE 불가. GraphE·FSell·VI 등 manual 항목의 실제 save→reload→fresh identity/version 보존은 미검증. |
| `PList.data_load`, double-click/확인 | 프로그램 코드별 `PLAY_LIST`를 ITEM_INDEX 순으로 읽어 playlist를 교체한다. | native `request-named-playlist-load`; resolver 기반 materialization | 화면/계약 구현. 실제 2,213행의 한국어 `DC_TITLE`/7필드 `LIST_TEXT` round-trip과 typed persistence는 전부 일치했다. KRX·해외종목·GraphE를 포함한 통합 read-only 증거는 `selectionMapped=1,603`, 차단 610이며 raw DB row는 scene data/asset/page를 재검증할 때까지 PREPARE 불가다. |
| `PList.button1_Click` | 프로그램 정의 삭제 | native `delete-named-playlist` | 화면 통합 완료. 실제 운영 DB-W 없음. |
| 취소 | 아무 변경 없이 닫는다. | mutation 요청 없음 | 화면 통합 완료. |
구현은 `src/MBN_STOCK_WEBVIEW.Core/Data/NamedPlaylistPersistence.cs`, KRX/NXT/해외종목/manual 전용 restore, `src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OracleNamedPlaylistMutationExecutor.cs`, `MainWindow.NamedPlaylists.cs`, `Web/named-playlist-workflow.js`에 있다. 자동 근거는 named playlist Core/Infrastructure/Web/bridge 테스트이며 실제 기존 데이터의 read-only 대조는 `docs/NAMED_PLAYLIST_READ_AUDIT.md`에 있다. 실제 운영 DB write와 named playlist 전용 scene/asset/PREPARE/PGM 검증은 아직 없다.
### ThemeA — 테마 추가·편집
원본: `Form/ThemeA.cs`, `Form/ThemeA.Designer.cs`
| 원본 이벤트·버튼 | 원본 입력·저장 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `Theme_Load` | KRX/NXT 선택, 기존 테마 구성 종목과 순서를 로드한다. | native `request-theme-catalog-list`, `request-operator-catalog-schema`; `operator-catalog-workflow.js`; `operator-catalog-ui.js` | 화면 통합 완료 / 실환경 검증 대기 |
| `btnC_Click` | 시장별 테마명 중복을 확인한다. | native list/identity validation | 화면 통합 완료 |
| 종목 검색·double-click | 같은 시장의 종목만 prefix가 포함된 구성 행으로 추가한다. | stock search + typed theme draft/preview | 화면 통합 완료 |
| `btn1/2/3/4_Click`, Delete key | 선택/전체 삭제, 위/아래 이동 | operator catalog theme draft operations | 화면 통합 완료 |
| `btnok_Click`, `GetNextCode` | 신규 코드를 만들거나 기존 `SB_LIST`/`SB_ITEM`을 교체하고 입력 순서를 저장한다. KRX와 NXT 저장소를 구분한다. | native `request-theme-catalog-next-code`, `create-theme-catalog`, `replace-theme-catalog`; atomic mutation executor | 화면 통합 완료. 실제 운영 DB-W/PGM 없음. |
| UC4 테마 삭제 | `SB_ITEM``SB_LIST` 삭제 | native `delete-theme-catalog` | 화면 통합 완료. |
구현은 `src/MBN_STOCK_WEBVIEW.Core/Data/ThemeCatalogPersistence.cs`, `src/MBN_STOCK_WEBVIEW.Core/Data/OperatorCatalogSchemaValidation.cs`, `src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OperatorCatalogMutationExecutor.cs`, `MainWindow.OperatorCatalogs.cs`, `Web/operator-catalog-workflow.js`다. 자동 근거는 theme catalog, schema, executor, Web workflow 테스트다. expected identity와 응답 correlation 계약은 구현했지만 실제 운영 DB에서 KRX/NXT, 동명이름, 코드가 같은 선택 행을 정확히 replace/delete하는지는 검증 대기다.
### EList — 전문가 신규 생성
원본: `Form/EList.cs`, `Form/EList.Designer.cs`
| 원본 이벤트·버튼 | 원본 입력·저장 경계 | 새 구현·bridge | 상태·검증 |
|---|---|---|---|
| `EList_Load`, `GetNextCode` | `EXPERT_LIST`의 다음 4자리 코드를 제안한다. | native `request-expert-catalog-next-code`; `operator-catalog-ui.js` | 화면 통합 완료 / 실환경 검증 대기 |
| `btnok_Click` | 이름을 검증하고 `EXPERT_LIST`에 신규 전문가를 저장한다. | native `create-expert-catalog` | 화면 통합 완료. 실제 운영 DB-W 없음. |
| UC6 저장·삭제 | 전문가와 `RECOMMEND_LIST` 구성 종목·금액·PLAYINDEX를 교체 또는 삭제한다. | native `replace-expert-catalog`, `delete-expert-catalog` | 화면 통합 완료. 실제 PGM 없음. |
| `btncn_Click` | 변경 없이 닫는다. | mutation 요청 없음 | 화면 통합 완료. |
구현은 `src/MBN_STOCK_WEBVIEW.Core/Data/ExpertCatalogPersistence.cs`, 공통 schema/executor/native partial, `Web/operator-catalog-workflow.js`다. 자동 근거는 expert catalog, schema, executor, Web workflow 테스트다. 전문가 코드·이름과 추천 행 identity 계약은 구현했지만 실제 운영 DB의 create/replace/delete read-back은 검증 대기다.
## bridge와 화면 로딩 상태 요약
| 기능군 | native request | workflow | 현재 `index.html`/`app.js` |
|---|---|---|---|
| 종목/고정/업종/비교/테마 조회/해외/전문가 조회/거래정지 | `search-stocks`, `request-industries`, `search-themes`, `request-theme-preview`, `search-world-stocks`, `search-overseas-industries`, `search-experts`, `request-expert-preview`, `search-trading-halts` | 각 전용 workflow | 로드·연결됨 |
| 이름 있는 플레이리스트 | `request-named-playlist-list`, `request-named-playlist-load`, `create-named-playlist`, `replace-named-playlist`, `delete-named-playlist` | `named-playlist-workflow.js` | 로드·연결됨 |
| GraphE | `request-manual-financial-list`, `request-manual-financial-load`, `request-manual-financial-selection`, `create-manual-financial-record`, `update-manual-financial-record`, `delete-manual-financial-record`, `delete-all-manual-financial-records` | `manual-financial-workflow.js`, `manual-financial-ui.js` | 로드·연결됨 |
| FSell/VI | `request-manual-operator-data-status`, `request-manual-net-sell-data`, `save-manual-net-sell-data`, `request-vi-manual-list`, `save-vi-manual-list` | `manual-lists-workflow.js`, `manual-lists-ui.js` | 로드·연결됨 |
| ThemeA/EList CRUD | `request-theme-catalog-list`, `request-expert-catalog-list`, `request-operator-catalog-schema`, theme/expert next-code, create/replace/delete requests | `operator-catalog-workflow.js`, `operator-catalog-ui.js` | 로드·연결됨 |
## 근거 파일 빠른 링크
| 영역 | 구현 | 자동 테스트 |
| 원본 이벤트 | 원본 의미 | LegacyParityApp 동작 |
|---|---|---|
| 공통 화면·송출·배경 | [MainWindow](../MainWindow.xaml.cs), [Playout partial](../MainWindow.Playout.cs), [Background partial](../MainWindow.Background.cs), [Web app](../Web/app.js) | [playout safety](../tests/Web/playout-safety.test.cjs) |
| 328 fixed / 44 industry / 31 stock / 9 comparison | [fixed workflow](../Web/legacy-fixed-workflow.js), [industry workflow](../Web/industry-workflow.js), [stock workflow](../Web/operator-workflow.js), [comparison workflow](../Web/comparison-workflow.js) | [fixed](../tests/Web/legacy-fixed-workflow.test.cjs), [industry](../tests/Web/industry-workflow.test.cjs), [stock](../tests/Web/operator-workflow.test.cjs), [comparison](../tests/Web/comparison-workflow.test.cjs) |
| 테마 조회·해외·전문가 조회·거래정지 | [theme](../Web/theme-workflow.js), [overseas](../Web/overseas-workflow.js), [expert](../Web/expert-workflow.js), [trading halt](../Web/trading-halt-workflow.js) | [theme](../tests/Web/theme-workflow.test.cjs), [overseas](../tests/Web/overseas-workflow.test.cjs), [expert](../tests/Web/expert-workflow.test.cjs), [trading halt](../tests/Web/trading-halt-workflow.test.cjs) |
| GraphE | [native partial](../MainWindow.ManualFinancial.cs), [Core service](../src/MBN_STOCK_WEBVIEW.Core/Data/ManualFinancialScreens.cs), [Oracle executor](../src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OracleManualFinancialMutationExecutor.cs), [workflow](../Web/manual-financial-workflow.js) | [Core](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyManualFinancialScreenServiceTests.cs), [Infrastructure](../tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/OracleManualFinancialMutationExecutorTests.cs), [Web](../tests/Web/manual-financial-workflow.test.cjs), [bridge](../tests/Web/manual-financial-bridge-integration.test.cjs) |
| FSell / VI | [native partial](../MainWindow.ManualLists.cs), [FSell store/data source](../src/MBN_STOCK_WEBVIEW.Infrastructure/Playout/S5025TrustedManualFileDataSource.cs), [VI store](../src/MBN_STOCK_WEBVIEW.Infrastructure/Playout/ViTrustedManualFileStore.cs), [workflow](../Web/manual-lists-workflow.js) | [FSell store](../tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/S5025TrustedManualFileStoreTests.cs), [VI store](../tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/ViTrustedManualFileStoreTests.cs), [Web](../tests/Web/manual-lists-workflow.test.cjs) |
| PList / AList | [native partial](../MainWindow.NamedPlaylists.cs), [Core service](../src/MBN_STOCK_WEBVIEW.Core/Data/NamedPlaylistPersistence.cs), [Oracle executor](../src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OracleNamedPlaylistMutationExecutor.cs), [workflow](../Web/named-playlist-workflow.js) | [Core integration](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyNamedPlaylistPersistenceIntegrationTests.cs), [Infrastructure](../tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/OracleNamedPlaylistMutationExecutorTests.cs), [Web](../tests/Web/named-playlist-workflow.test.cjs), [bridge](../tests/Web/named-playlist-bridge-integration.test.cjs) |
| ThemeA / EList | [native partial](../MainWindow.OperatorCatalogs.cs), [theme persistence](../src/MBN_STOCK_WEBVIEW.Core/Data/ThemeCatalogPersistence.cs), [expert persistence](../src/MBN_STOCK_WEBVIEW.Core/Data/ExpertCatalogPersistence.cs), [executor](../src/MBN_STOCK_WEBVIEW.Infrastructure/Execution/OperatorCatalogMutationExecutor.cs), [workflow](../Web/operator-catalog-workflow.js) | [theme](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyThemeCatalogPersistenceServiceTests.cs), [expert](../tests/MBN_STOCK_WEBVIEW.Core.Tests/LegacyExpertCatalogPersistenceServiceTests.cs), [Infrastructure](../tests/MBN_STOCK_WEBVIEW.Infrastructure.Tests/OperatorCatalogMutationExecutorTests.cs), [Web](../tests/Web/operator-catalog-workflow.test.cjs) |
| `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 불변 |
| 검색 버튼/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 차단 |
| 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 대체와 클릭 규칙
| 항목 | 결과 |
|---|---|
| 자동 테스트 | Core 1,383개, Infrastructure 173개, Playout 393개가 Debug/Release x64에서 각각 통과했고 Web workflow·bridge 415/415도 통과했다. 412 action의 폐쇄형 매트릭스, 계약·경계·복원 실패와 native fail-closed gate를 검증한다. |
| 실제 운영 DB write | **미실행.** GraphE, named playlist, ThemeA, EList의 mutation executor는 fake/계약 테스트로 검증했으며 운영 DB에는 쓰지 않았다. FSell/VI는 테스트용 격리 저장소만 사용했다. |
| 실제 Tornado2 PGM | 제한된 승인 회차에서 `5001`, `5074` page 1·2, TAKE OUT을 확인했다. 이는 전체 412 action, GraphE, FSell, VI, ThemeA, EList, 이름 있는 playlist 각각의 UI 동등성 검증이 아니다. |
| PList legacy 복원 | 실제 `DC_TITLE`/`LIST_TEXT` 2,213행의 한국어·7필드 무손상과 typed persistence 일치를 확인했다. 기본 `selectionMapped=1,581`/차단 632와 별도 GraphE 22/14를 합친 결과는 1,603/610이다. selection 매핑은 scene data·asset·PREPARE·PGM 완료 증거가 아니다. |
| manual named save | 현재 legacy PList manual 36행은 모두 GraphE이며 fresh proof로 22행을 복원하고 저장값 무효 9행·identity 중복 5행을 차단했다. FSell/VI PList행은 0건이고 production source read는 FSell 3×5행, VI 9항목·2 page로 전후 불변이다. 실제 Oracle save→reload와 PGM은 미검증이다. |
| ThemeA/EList identity | expected identity·상관 응답 계약은 구현했다. 실제 운영 DB의 KRX/NXT·동명이름·코드 경계와 정확한 선택 행 replace/delete는 미검증이다. |
| legacy file import | `종목비교.dat`는 signed package에서 8쌍 1회 실행·재시작 marker까지 확인했다. FSell·VI importer는 strict parser, source/hash 재검증, durable intent/marker와 격리 fixture를 통과했으며 현재 운영 profile은 기존 동일 파일 때문에 no-overwrite 차단된다. |
| 남은 실환경 검증 | 다섯 전용 화면의 Web 연결과 signed `1.0.5.0` Release MSIX package-context 실행은 화면/계약 검증 범위에서 확인됐다. 1.0.5 package UI에서 실제 삼성전자 검색·현재가 5001 DryRun과 자동 refresh cap을 확인했으며, 기존 10개 업무 탭 상태·비교 8쌍 영속성 증거는 1.0.4 감사에 보존한다. 실제 입력→운영 DB write→재조회와 각 전용 scene PGM 검증은 별도 승인·테스트 데이터·rollback 회차에서만 수행한다. |
FarPoint UI는 Web table/list로 표현한다. 다음 의미는 원본과 같이 유지한다.
- plain/Ctrl/Shift/Ctrl+Shift 선택과 현재 행
- column header 정렬과 현재 표시 순서
- 위/아래 이동, 선택/전체 삭제와 enabled 토글
- 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를 추정하지 않는다.
## 원클릭 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을 추측해 지우지 않는다.
## 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) |
| 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) | 최종 전체 회귀 기록란 포함 |
최종 Debug/Release 전체 테스트 개수, 새 MSIX SHA-256와 개발 PGM 결과는 병렬 변경을
모두 합친 뒤 위 검증 문서에 기록했다. 이전 package 숫자나 hash를 현재 결과로
재사용하지 않았다.