Files
MBN_STOCK_WEBVIEW/docs/LEGACY_BEHAVIOR_PARITY_WORKLOG.md

38 KiB

MBN_STOCK_N 동작 동등성 작업 기록

문서 성격: 이 문서는 역사·상세 증거를 보존한다. 현행 완료·차단 판정은 MIGRATION_STATUS.md가 우선한다. 아래의 “미실행”, “대기”, “별도 승인” 표현과 과거 테스트 수치는 해당 시점의 기록이며 현재 상태가 아니다.

2026-07-18 이후 현재 개발 PC의 Tornado2/PGM 작업에는 개발 환경 상시 권한을 적용한다. 아래의 과거 회차별 승인 기록은 당시 증거이며 새 개발 작업의 반복 승인 요구로 재사용하지 않는다.

기준 원본은 C:\Users\MD\source\repos\MBN_STOCK_N이며 항상 읽기 전용으로 분석한다. 대상은 MBN_STOCK_WEBVIEW.LegacyParityApp이다. 화면 모양만 비슷하게 만드는 것이 아니라 원본 이벤트의 입력, 상태 전이, DB 조회, K3D 호출 순서와 화면 피드백을 코드 단위로 대조한다.

판정 절차

  1. 원본 이벤트 핸들러와 관련 필드를 추적한다.
  2. 정상 동작과 명백한 원본 버그를 구분한다.
  3. 정상 동작을 특성화 테스트로 고정한다.
  4. C# authoritative state와 WebView 표시를 함께 수정한다.
  5. 안전 게이트를 유지한 상태에서 Debug/Release x64 빌드와 전체 테스트를 실행한다.

Slice 1: 선택 행, F8, NEXT, TAKE OUT

상태: 코드 및 자동 테스트 완료, 실제 PGM 명령은 실행하지 않음.

원본 근거:

  • MainForm.cs:1078 PlayList_CellClick
  • MainForm.cs:1915 btnTake_Click
  • MainForm.cs:2020 ONAirMode
  • MainForm.cs:2081 btnTakeout_Click
  • MainForm.cs:2120 btnNext_Click
  • MainForm.cs:2176 Next_Scene
  • MainForm.cs:2495 Show_PlayList

고정한 계약:

  • 운영자 선택 후보와 현재 PROGRAM 행을 독립 상태로 유지한다.
  • F8은 IDLE/PREPARED/PROGRAM 모두에서 그 시점의 선택 후보를 새로 Prepare 후 Play한다.
  • 선택 후보가 비활성이면 아래 방향의 첫 활성 행만 사용하며 순환하지 않는다.
  • NEXT는 선택 후보가 아니라 현재 PROGRAM 행을 기준으로 Page NEXT 또는 다음 활성 컷을 결정한다.
  • 선택 후보는 회색, 현재 PROGRAM 행은 분홍으로 동시에 표시한다.
  • Page NEXT 결과를 상태 표시뿐 아니라 플레이리스트 Page 셀에도 남긴다.
  • F8/PREPARE는 페이지형 컷을 page 1로 다시 시작한다.
  • 마지막 항목에서는 NEXT를 비활성화하고 vendor 명령을 보내지 않는다.
  • TAKE OUT은 StopAll 범위이며 IDLE에서도 연결된 엔진에 긴급 정리를 요청할 수 있다.

남은 세부 감사:

  • Page 총수를 행에 저장된 원본 current/total에서 고정할지, PREPARE 시 fresh DB 결과로 다시 계산할지에 대한 동등성은 PageN 전용 Slice에서 다시 판정한다.
  • column 3의 정확한 한글 문자열로 Page NEXT를 분기하던 원본 조건과 typed builder 식별자를 사용하는 현재 조건의 차이도 같은 Slice에서 검증한다.

유지한 안전 차이:

  • SDK Play callback이 pending이면 TAKE OUT 이외 명령을 보내지 않는다.
  • timeout 또는 OutcomeUnknown은 재시도하지 않고 격리한다.
  • 결과가 확인되기 전에는 새 PROGRAM 행으로 확정하지 않는다.
  • PROGRAM 중 구조 변경은 현재 단계에서 계속 차단한다.

재현하지 않은 원본 버그:

  • 마지막 컷에서 이전 m_ShowIndex 때문에 같은 scene을 다시 Play하는 동작
  • catch로 잘못된 page 문자열과 DB/엔진 오류를 숨기는 동작
  • Home/End가 timer와 색상 상태를 깨뜨리는 동작
  • 고정 크기 s_buf[100]에 의존하는 Space 토글

Slice 2: 체크박스, 키보드, 이동, 삭제, drag

상태: 코드 및 자동 테스트 완료, 변경을 합친 package의 실제 마우스 검증은 아직 수행하지 않음.

원본 근거:

  • MainForm.cs:1078 PlayList_CellClick
  • MainForm.cs:1464 btnup_Click
  • MainForm.cs:1579 Spd_UpDown
  • MainForm.cs:1649 btndelete_Click
  • MainForm.cs:1730 btntdelete_Click
  • MainForm.cs:3140 checkBox1_CheckedChanged
  • MainForm.cs:3622 PlayList_DragDrop
  • MainForm.cs:3805 PlayList_KeyPress
  • MainForm.cs:3887 ProcessCmdKey
  • MainForm.Designer.cs:1598 AllowRowMove

고정한 계약:

  • F8 후보, 현재 PROGRAM, Ctrl/Shift 삭제 집합과 키보드 focus를 서로 다른 상태로 유지한다.
  • F8/PREPARE 성공과 disabled-row skip은 실제 F8 후보만 옮기며 bufdel에 해당하는 삭제 집합과 m_newrow/m_oldrow에 해당하는 Shift anchor를 바꾸지 않는다.
  • 체크박스 열을 누르면 삭제 집합과 Shift anchor는 그대로 두고 그 행만 F8 후보가 된다.
  • Space는 포커스된 실제 행의 현재 enabled 값을 반전하며 별도 100칸 배열을 사용하지 않는다.
  • Home/End는 원본처럼 자동 갱신 timer를 먼저 멈추고 첫/마지막 행을 후보로 만들며 그 행으로 scroll한다. DEL 집합, Shift anchor, 현재 입력/ListBox focus, PROGRAM과 Page 표시는 바꾸지 않는다.
  • Up/Down은 마지막 활성 행 하나만 인접 행과 교환하고 이동한 stable row identity를 후보로 유지한다.
  • playlist drag도 Ctrl/Shift 집합 전체가 아니라 실제 drag source 한 행만 before/after로 이동한다.
  • 선택 삭제 뒤 기존 후보가 살아 있으면 같은 row identity를 유지하고, 삭제됐으면 다음 생존 행, 다음 행이 없으면 이전 생존 행을 명시적인 후보로 만든다.
  • 빈 목록에 첫 행을 추가하거나 DB playlist를 불러오면 첫 행이 화면의 후보와 실제 F8 대상이 되지만, 실제 행 클릭 전에는 DEL 집합에 암묵적으로 들어가지 않는다. 화면에 후보가 없는데 내부적으로 0번 행을 재생하던 fallback은 제거했다.
  • PREPARED/PROGRAM/pending/OutcomeUnknown에서는 enabled, 이동, 삭제, row/cut drag를 Web과 native 양쪽에서 차단한다. 후보 선택과 컷 단순 선택은 로컬 상태이므로 허용한다.

재현하지 않은 원본 버그:

  • bufdel[100], s_buf[100]의 제한, 중복 인덱스와 stale selection
  • 선택 삭제가 PROGRAM 행이나 현재 송출 순서를 손상하는 동작
  • 이동·삭제 뒤 m_rowidx, m_newrow, m_crow 숫자 인덱스가 다른 항목을 가리키는 동작
  • Home/End 뒤 focus와 후보가 서로 다른 행을 가리키는 동작
  • 컷 drag payload를 무시하거나 playlist 다중 삭제 집합을 다중 행 drag로 확대하는 동작

Slice 3: PageN 문자열 분기와 저장 total

상태: 코드 및 집중 자동 테스트 완료, 실제 PGM 명령은 실행하지 않음.

원본 근거:

  • MainForm.cs:2120 btnNext_Click
  • MainForm.cs:2176 Next_Scene
  • MainForm.cs:2495 Show_PlayList

고정한 계약:

  • 실제 operator row는 column 3 GraphicType이 정확히 5단 표그래프, 6종목 현재가, 12종목 현재가일 때만 같은 행 Page NEXT를 허용한다.
  • 위 세 문자열은 각각 5·6·12개 단위와 연결하고, row의 current/total 중 total을 1..20 범위에서 읽어 PREPARE부터 TAKE OUT까지 고정한다.
  • on-air 중 DB 결과가 늘어도 저장 total 뒤의 새 page를 노출하지 않고, 줄어든 결과로 현재/다음 page를 만들 수 없으면 vendor NEXT 전에 fail-closed한다.
  • 같은 paged scene code로 해석되더라도 column 3 문자열이 정확하지 않으면 일반 행으로 취급하여 다음 enabled playlist row로 이동한다.
  • paged row의 마지막 page에서 다음 playlist row로 이동한 뒤에는 원본처럼 자동 갱신을 다시 시작하지 않는다. 일반 행에서 다음 row로 이동한 경우에만 다시 시작한다.
  • 요구사항의 20 page cap, 0건 fail-closed, 마지막 row에서 재생 반복 금지는 유지한다. 원본의 101/121/241건에서 21 page가 되는 off-by-one, 1/0, stale scene 재생은 명시적인 안전 교정이며 문자 그대로 재현하지 않는다.
  • timer refresh의 새 구현은 불완전할 수 있는 GetPlayingScene proxy가 아니라 adapter가 보존한 on-air scene reference를 갱신한다는 실제 구현 계약으로 주석과 테스트 이름을 바로잡았다.

자동 검증:

  • Core: 저장 total 고정, DB 증가/감소, refresh, 없는 다음 page 차단, 비정규 문자열의 playlist NEXT, metadata 없는 직접 Core 호출의 기존 fresh-plan 동작.
  • Application: 세 정확한 문자열의 5·6·12 mapping, suffix total 고정, 유사 문자열 차단, 0/21 및 잘못된 current/total 거부.

Slice 4: 종목 검색과 런타임 컷 메뉴

상태: 코드 및 자동 테스트 완료, 실제 DB/Tornado 명령은 실행하지 않음.

원본 근거:

  • MainForm.cs:399, MainForm.cs:596 종목.ini/INI_parser
  • MainForm.cs:844 button9_Click
  • MainForm.cs:859 searchStock
  • MainForm.cs:3039 txtSearch_KeyPress
  • MainForm.cs:3427 listView1_MouseDown
  • MainForm.cs:3534 listView1_MouseMove
  • MainForm.cs:3635 PlayList_DragDrop

고정한 계약:

  • 빈 검색 버튼은 기존 결과·종목 선택·컷 선택을 유지하고, 빈 Enter만 전체 조회를 실행한다.
  • 새 조회는 종목 선택만 지우며 컷 선택은 유지한다. 이후 새 종목을 선택해 drag/drop하면 남아 있던 컷을 새 종목 identity로 추가한다.
  • 컷 Ctrl/Shift, 정확한 좌표와 500ms 미만 double-click, 물리 순서 drag, 중복 drop을 유지한다.
  • 일반 시장의 - 구분행은 무시하고 NXT에서는 원본 경고 modal 순서를 유지한다.
  • CP949 종목.ini%LOCALAPPDATA% 외부 override, package 후보 순서로 읽는다. 둘 다 없거나 유효하지 않으면 검증된 31개 내장 목록으로 폴백한다. 외부 파일이 존재하지만 잘못됐으면 다른 파일로 우회하지 않고 즉시 폴백한다.
  • 최대 50개 물리행, 64 KiB, 정확한 [종목], 알려진 컷과 구분행만 허용하고 부분 결과와 실제 경로는 상태에 노출하지 않는다. 실제 INI 파일은 Git/MSIX에 넣지 않는다.
  • 런타임 권위본은 원본의 bin\Debug\Res\종목.ini이며 현재 내장 fallback과 SHA-256이 같다. 이 파일과 s5001이 사용하는 시간외단일가를 유지한다. 소스 트리 Res\종목.ini의 서로 다른 시간외종가 표기는 실제 실행 기준으로 되돌리지 않는다.

Slice 5: 전역 키, 배경과 DissolveTime

상태: 코드 및 자동 테스트 완료, 실제 PGM 명령은 실행하지 않음.

원본 근거:

  • MainForm.cs:964 MainForm_KeyDown
  • MainForm.cs:3887 ProcessCmdKey
  • MainForm.cs:4008 btnback_Click
  • MainForm.cs:4037 ckback_CheckedChanged
  • MainForm.cs:488, MainForm.cs:1059 ComboDi.SelectedIndex

고정한 계약:

  • 원본 child form에는 CancelButton/KeyPreview/Escape handler가 없고 ShowDialog가 MainForm을 비활성화한다. 따라서 modal의 Escape는 form을 닫지도 TAKE OUT을 보내지도 않으며, F2/F3/F8과 playlist 전역 키도 owner로 전달하지 않는다.
  • Delete는 원본 KeyPreview처럼 playlist 삭제 intent를 보내되 기본 키를 억제하지 않아, 텍스트 입력이 focus를 가진 경우 문자 삭제도 정상 동작한다.
  • 시작 기본 배경은 Cuts\video\기본.vrv, F2/F3 기본 배경은 Cuts\..\배경\기본.vrv로 분리하고 두 신뢰 root가 서로 대신하지 않게 했다.
  • 원본 DissolveTime을 소유한 panel3은 Designer에서 항상 숨겨져 있으므로 WebView selector도 hidden/disabled로 유지하고 운영자 변경 intent를 노출하지 않는다. C#의 기존 0..19 인덱스와 기본값 6은 scene effect 상태로만 보존한다.

Slice 6: 탭 재진입과 dragover 교환

상태: 코드 및 자동 테스트 완료, 실제 DB/Tornado 명령은 실행하지 않음.

원본 근거:

  • MainForm.cs:2937 tabControl1_SelectedIndexChanged
  • MainForm.cs:3183 tab_MouseDown
  • MainForm.cs:3214 tab_MouseMove
  • MainForm.cs:3234 tab_DragOver

고정한 계약:

  • 코스피/코스닥은 재진입마다 목록을 다시 조회하고 선택·비교쌍을 초기화한다.
  • 비교는 저장된 CP949 비교쌍을 매 재진입마다 다시 읽고, 해외종목·거래정지는 검색 결과, 선택과 정렬 상태를 새 UC처럼 초기화한다. MA5/MA20은 MainForm 전역 상태로 유지한다.
  • 폐기된 탭의 늦은 비동기 결과는 activation generation으로 무시한다.
  • 좁은 탭을 넓은 탭 오른쪽으로 옮길 때는 target.right - source.width 초과, 왼쪽은 target.left + source.width 미만에서만 교환한다. 같거나 넓은 source는 진입 즉시 교환한다.
  • 같은 crossing의 반복 dragover와 busy 거부가 중복/누락 교환을 만들지 않게 했다.

Slice 7: UC1 트리 이벤트와 이동평균선 샘플링

상태: 코드 및 자동 테스트 완료, 실제 DB/Tornado 명령은 실행하지 않음.

원본 근거:

  • Control/UC1.cs:121-139, 1303-1340, 1393-1407
  • Scene/s8010.cs:431-453, 765-807
  • MainForm.cs:2495+, 2937-2999

고정한 계약:

  • 루트 라벨 단일 클릭은 선택만 하며 disclosure만 펼침/접기를 수행한다.
  • 루트·리프 더블클릭은 펼침 상태를 바꾸지 않고 C# action을 정확히 한 번 보낸다.
  • 같은 탭의 상태 render는 open/selection/focus/scroll을 보존하고, 실제 탭 재진입은 원본 UC1처럼 Expand=true, 전체 펼침, 첫 루트 선택·focus·scroll로 초기화한다.
  • Busy/고정 섹션 배치 중에는 action뿐 아니라 로컬 disclosure 변경도 차단한다.
  • MA5/MA20은 행 추가 시 저장하지 않고 s8010 PREPARE, F8, NEXT, 자동 갱신의 장면 생성 시점마다 현재 전역 체크 상태를 다시 읽는다. named playlist identity에도 MA 플래그를 저장하지 않는다.
  • 공용 UC1의 부모 일괄 추가 중 부분 mutation은 재현하지 않는다. 다만 업종 탭의 부모는 실제로 22개 루프 전에 uc_3 == null 예외가 발생해 아무 행도 추가하지 않으므로, 이 항목은 Slice 12의 재감사 결과에 따라 실제 관찰 동작인 no-op으로 고정했다.

상세 근거와 결함 제외는 docs/UC1_TREE_EVENT_PARITY.md에 기록했다.

Slice 8: K3D 원시 인자, callback 게이트, 컷 ListView 경계

상태: 코드 및 자동 테스트 완료, 실제 PGM 명령은 실행하지 않음.

원본 근거:

  • MainForm.cs:2628-2639 공통 SetBackgroundVideo(path, 2004, 10)
  • MainForm.csSetSceneEffectType(10, FADE, ComboDi.SelectedIndex) 호출
  • MainForm.cs:2651+, 1992의 3,000,000초 Int32 곱셈/Timer.Interval 결과
  • MainForm.cs:3427-3525, 3566-3588, 3635-3643 컷 선택·드롭
  • MainForm.cs:3887-3906 Home/End 후 base key 처리

고정한 계약:

  • K3D scene effect 첫 인자는 bool로 정규화하지 않고 원본 raw 10을 전달한다.
  • 공통 배경은 raw (2004, 10), s5006 장면 내부 배경은 (1, 1)을 구분한다.
  • OnCutOut/OnStopAll 완료 전에는 PREPARE, F8, NEXT, 갱신과 중복 TAKE OUT을 SDK에 보내지 않고 Web 버튼과 운영 데이터 mutation도 잠근다. pending Play 중 비상 TAKE OUT 1회 경로는 유지한다.
  • 5076/5079/5080/5081은 원본의 unchecked Int32 overflow 뒤 Timer.Interval 예외 때문에 실제 자동 갱신이 시작되지 않으므로 34.7일 타이머로 바꾸지 않고 명시적으로 비활성화한다.
  • 런타임 컷 메뉴 bridge 범위를 원본 물리행과 같은 0..49로 통일한다.
  • 연속 Shift/Ctrl+Shift는 Win32 ListView selection mark를 고정하고, 빈 영역 클릭은 선택을 지우되 native anchor는 유지한다. 빈 영역 뒤 stale 컷 drop은 0행이다.
  • PREPARED/PROGRAM 중 컷 pointer double-click의 암묵적 행 추가를 native/Web 양쪽에서 차단한다.
  • Home/End의 playlist/timer 부작용 뒤에도 검색 입력 caret과 검색결과 기본 선택 동작을 보존한다.
  • DissolveTime의 모든 외부·Core 경계는 원본 ComboBox index인 0..19만 허용한다.

Slice 9: 종목 ListBox와 컷 ListView 키보드

상태: 코드 및 자동 테스트 완료, 실제 package 키보드 검증은 아직 수행하지 않음.

고정한 계약:

  • 종목 결과는 하나의 tab stop을 갖고 ArrowUp/Down, Home/End와 10행 viewport의 PageUp/PageDown 9행 이동을 재현한다. double-click도 선택만 바꾸고 컷을 추가하지 않는다.
  • 종목 Home/End는 결과 선택과 focus를 보존한 채 전역 playlist 경계 부수 효과도 실행한다.
  • 컷 목록은 물리행과 separator를 포함해 ArrowUp/Down, Home/End, Space를 처리한다.
  • 컷 Ctrl/Shift/Ctrl+Shift는 Win32 selection mark 규칙을 C# 권위 상태에서 적용하고 키보드 선택은 마우스 전용 selectedRowsList에 섞지 않는다. drop은 native 선택 집합을 사용한다.
  • PREPARED/PROGRAM/pending/unknown 또는 종목 미선택 상태에서는 컷 선택과 focus 이동을 함께 차단해 DOM과 C# 상태가 어긋나지 않게 한다.

Slice 10: modal 소유권·PList 완료 순서와 컷 drag 임계값

상태: 코드·자동 테스트·unsigned Release package-context 시작/정상 종료 완료. 실제 컷 drag 마우스 실측과 DB 쓰기/Tornado 명령은 실행하지 않음.

원본 근거:

  • Form/PList.cs:173-196, 222-237 행 double-click과 확인 버튼 save/load/close 순서
  • Form/PList.cs:184, 226, 229저장하겠습니까?, 저장되었습니다
  • Form/*Designer.cs의 child form에 없는 CancelButton, KeyPreview, Escape handler
  • MainForm.cs:3427-3459, 3534-3549, 3635-3643 컷 mouse-down rectangle, drag 시작과 체크 행 physical-order drop
  • MainForm.Designer.cs:1391-1399 숨겨진 panel3

고정한 계약:

  • named playlist, GraphE, FSell/VI, ThemeA/EList modal은 opener를 기억하고 최초 legacy control에 focus하며 Tab/Shift+Tab을 modal 안에 가둔다. 중첩 alert를 닫으면 child modal의 이전 control로, child modal을 닫으면 owner control로 focus를 돌려준다.
  • child modal의 Escape는 원본처럼 no-op으로 소비하고 MainForm TAKE OUT으로 새지 않는다.
  • PList save는 정확히 저장하겠습니까?를 묻는다. 확인 버튼 save 성공만 저장되었습니다를 표시하고, 행 double-click save는 성공 알림 없이 닫는다. No를 선택해도 원본의 unconditional Close()처럼 modal을 닫는다.
  • 비동기 DB 명령 중에는 원본 동기 ShowDialog 호출이 반환되지 않던 구간처럼 close/cancel과 중복 명령을 잠근다. 정확한 command/requestId/definition/sequence receipt만 modal을 닫으며 실패 또는 결과 불명확 상태에서는 성공 알림·추정 close를 하지 않는 안전 교정을 유지한다.
  • PList.Get_FileName_Folder가 새 DataSource를 지정할 때 non-empty ListBox의 첫 행이 자동 선택되는 동작을 열기·새로고침·추가·삭제 뒤 모두 유지한다. 다른 행 click은 즉시 시각 선택을 바꾸되 exact native selection receipt 전에는 확인·삭제를 잠그고, double-click은 같은 opaque definition id에 묶어 receipt 뒤 한 번만 실행하므로 이전 첫 행을 잘못 저장/삭제하지 않는다.
  • DB commit이 확정되고 read-back만 실패한 CommittedOptimistic은 원본처럼 저장 완료로 처리하되 warning은 유지한다. 반대로 이전 성공 outcome이 남은 채 현재 입력 검증이 실패한 경우에는 named workflow revision이 증가하지 않으므로 성공 receipt·알림·close를 만들지 않는다.
  • category tree 검색 input을 state render가 다시 만들더라도 id, caret/selection 범위와 focus를 복원해 Enter 검색 뒤 원본 textbox의 연속 입력 동작을 유지한다.
  • 컷 drag rectangle은 Point(x-SM_CXDRAG, y-SM_CYDRAG)와 Windows drag size를 사용하고 left/top 포함, right/bottom 제외 Rectangle.Contains 판정을 그대로 적용한다. 브라우저 native drag 임계값은 사용하지 않으며 pointer capture 뒤 playlist 경계 안에서 놓았을 때만 기존 DropSelectedCutsOnPlaylist를 한 번 호출한다.
  • SM_CXDRAG/SM_CYDRAG는 DPI 비인지 GetSystemMetrics 값을 그대로 쓰지 않는다. GetSystemMetricsForDpi(..., 96)로 XAML host DIP 값을 읽고 XamlRoot.RasterizationScale도 state에 싣는다. Web은 이를 window.devicePixelRatio와 대조해 과거 profile에 남은 browser zoom까지 제거한 pointer-event CSS pixel 임계값을 계산한다. 100%/150% DPI와 retained zoom을 자동 테스트하며 비정상 scale 비율은 1로 fail-safe한다.
  • WM_SETTINGCHANGE/WM_DISPLAYCHANGE/WM_DPICHANGED와 모든 state 게시 경계에서 값을 다시 읽어 시작 시 한 번만 캐시하지 않는다. cut-pointer-down 회신이 도착했을 때 아직 시작되지 않은 pointer gesture의 rectangle도 최신 값으로 다시 만든다. 이후 browser/pinch zoom 입력은 비활성화하고 API 실패나 0 이하는 축별 4 DIP로 fail-safe한다. 실제 drop 내용은 payload가 아니라 C#의 현재 선택 집합과 physical 순서가 계속 권위본이다.

Slice 11: UC3 종목 비교 중복행과 전체 삭제

상태: 코드 및 자동 테스트 완료, 실제 파일·DB·Tornado 명령은 실행하지 않음.

원본 근거:

  • Control/UC3.cs:100-145 비교쌍 추가와 File_Save
  • Control/UC3.cs:147-154 선택 행 삭제
  • Control/UC3.cs:156-168 전체 삭제 Yes/No
  • Control/UC3.cs:171-183 Up/Down 저장

고정한 계약:

  • 원본 btnAdd_Click에는 중복 검사가 없으므로 같은 두 대상을 같은 순서로 여러 번 추가할 수 있다. 각 행은 별도 stable row ID를 가지며, 이동·선택 삭제는 그중 활성 행 하나에만 적용한다.
  • 화면의 비교 조작 표시는 원본 Designer와 같은 ◀▶, 추가, UP, DOWN, DEL, DEL ALL을 사용하고, 원본에 없던 별도 초기화 버튼은 표시하지 않는다.
  • CP949 parser/import도 같은 비교쌍의 반복을 허용하되 물리 순서, 연속 RowNumber, 최대 행 수와 형식·인코딩 검증은 유지한다. 저장 후 재로드해도 반복 행과 순서가 보존된다.
  • DEL ALL은 Web의 window.confirm이나 직접 파일 변경으로 처리하지 않는다. C#이 정확한 MmoneyCoder caption과 모두 삭제하겠습니까? 문구의 Yes/No 상태를 소유하며, No는 0회, Yes는 confirmation을 먼저 소비한 뒤 정확히 1회만 mutation한다.
  • confirmation 동안에는 화면 재연결용 ready와 confirm/cancel 이외의 bridge intent를 모두 무시한다. 이는 원본의 modal MessageBox가 MainForm과 다른 child action을 막던 동작을 native 경계에서도 보장한다.
  • Yes/No bridge payload는 빈 객체만 허용하며 row ID, 종목 identity 또는 action kind를 받지 않는다. 삭제 대상 목록의 revision이 바뀌면 저장하지 않는다.

재현하지 않은 원본 버그:

  • 원본 File_Save는 RowCount가 0이면 파일을 쓰지 않아 마지막/전체 삭제가 재시작 뒤 되살아난다. 새 구현은 빈 CP949 파일을 원자적으로 1회 저장해 삭제를 영속화한다. 이는 기존 one-time import와 재시작 영속성 계약을 깨지 않기 위한 명시적 데이터 안전 교정이다.

Slice 12: UC2 업종 목록·비교 입력과 업종 UC1 단일 루트

상태: 코드 및 자동 테스트 완료, 실제 DB/Tornado/UI 명령은 실행하지 않음.

원본 근거:

  • Control/UC2.cs:32-76 목록 조회, 즉시 선택, double-click 회전과 텍스트 swap
  • Control/UC2.Designer.cs:43-106 목록 위/비교 입력 아래 배치와 편집 가능한 txt1/txt2
  • Control/UC1.cs:154-180, 204-220, 330-351 업종 루트·리프와 1열/2열 조건
  • MainForm.cs:2966-2985 업종 UC1을 uc_3 == null로 생성하는 실제 경로

고정한 계약:

  • 업종 액션 트리는 원본 INI처럼 [코스피] 또는 [코스닥] 루트 하나와 원본 순서의 자식 22개만 표시한다. 기존 Web의 인위적인 5개 그룹은 제거했다.
  • 업종 루트 double-click은 공용 UC1이 22개 루프 전에 null인 uc_3를 읽고 바깥 catch가 예외를 삼키므로 실제 바이너리처럼 조용한 no-op이다. 자식 double-click만 한 컷을 추가한다.
  • 목록 single-click은 지연 없이 C# 활성행을 바꾸고, state 회신은 기존 DOM을 교체하지 않아 같은 행의 두 번째 click과 double-click을 잃지 않는다.
  • 목록 전체가 하나의 tab stop이며 ArrowUp/Down, Home/End가 원본 ListBox처럼 활성행을 즉시 바꾼다. 행 버튼은 개별 Tab 순서에 들어가지 않는다.
  • 화면 배치는 목록, 업종비교, txt1/교환/txt2 순서이고 교환 버튼은 원본 ◀▶ 표기다.
  • txt1/txt2는 편집 가능하다. Web은 slot/value만 닫힌 intent로 보내며 C#의 FirstTextSecondText가 권위 상태를 가진다. action intent에는 입력 문자열을 섞지 않는다.
  • 목록 double-click은 수동 txt1을 txt2로 민 뒤 새 업종명을 txt1에 넣고, 교환은 한 칸만 채워진 경우까지 두 문자열을 그대로 바꾼다. 1열판은 계속 활성 목록 행을 사용하고 2열판만 두 편집 문자열을 사용한다.
  • 입력 state 회신은 focused input을 덮거나 workspace를 교체하지 않아 blur 직후 swap이나 자식 double-click이 사라지지 않는다.
  • 원본 쿼리가 돌려준 중복 이름·코드 행은 500행 안전 상한 안에서 순서 그대로 표시하고 클릭한 인덱스로 구분한다. 장면 데이터 조회의 exact-one-row 검증은 유지해 모호한 결과는 PREPARE 전에 fail closed한다.

명시적 안전 교정:

  • 원본 TextBox 기본 최대 길이 대신 실제 scene selector 경계인 128자로 제한하고 control, surrogate, U+FFFD를 거부한다. 편집 중 빈 값은 허용하되 2열판 생성 때 공백뿐인 값과 subject 구분자인 -가 든 값은 거부한다. SQL은 계속 parameterized equality로만 실행된다.

Slice 13: UC4 테마 검색·세션·미리보기

상태: 코드 및 자동 테스트 완료, 실제 DB/Tornado/UI 명령은 실행하지 않음.

원본 근거:

  • Control/UC4.cs:109-247, 429-439 테마 검색, 선택, 현재가·예상체결가와 6·12종목 버튼
  • Control/UC4.Designer.cs 검색 → 결과 → 정렬 → 액션 → 종목 미리보기의 세로 배치
  • Control/UC4.cs NXT 조회의 현재 시각 기준 오전/오후 세션 선택

고정한 계약:

  • UC4 검색 intent는 검색어만 받는다. 원본 화면에 없던 NXT 세션 선택기는 제거했고, C#이 로컬 시각 012시에는 장전, 1323시에는 장후 세션을 탭 진입과 검색 시점마다 결정한다.
  • 화면은 검색 입력, 테마 결과, 정렬 라디오, 6종목, 12종목, 현재가, 예상체결가, 종목 미리보기 순서의 한 열 구조다. 결과·미리보기의 인위적인 좌우 2열 구조는 제거했다.
  • 검색 결과는 원본처럼 KRX 결과 다음 NXT 결과이며 각 DB 결과 순서를 그대로 유지한다. Web은 결과 ID와 action ID만 전송하고 테마 코드·종목 코드·세션을 만들지 않는다.
  • 결과 클릭은 C# receipt를 기다리지 않고 즉시 선택 표시를 바꾸되 opaque ID를 정확히 한 번만 보낸다. 동일 구조의 receipt는 결과 DOM을 교체하지 않아 연속 클릭과 focus를 잃지 않는다.
  • NXT 테마의 예상체결가 버튼은 Web에서 임의로 비활성화하지 않는다. 클릭하면 원본과 같은 NXT는 데이터가 없습니다. 안내를 native 경계에서 표시하고 플레이리스트 mutation은 0회다.
  • 플레이리스트에는 원본의 표시 열과 내부 scene identity를 분리해 저장하고, 미리보기가 0행인 선택으로는 빈 컷을 만들지 않는다.

명시적 안전 교정:

  • DB 검색·미리보기 결과는 generation과 opaque selection ID로 연결한다. 늦게 끝난 이전 검색이 새 결과나 선택을 덮지 못하며, 동일 이름이 모호하거나 미리보기가 비어 있으면 fail closed한다.

Slice 14: UC5 해외지수·해외업종·해외종목

상태: 코드 및 자동 테스트 완료, Debug/Release x64와 unsigned MSIX 생성 완료. 실제 DB/Tornado 명령은 실행하지 않음.

원본 근거:

  • Control/UC5.cs:33-47, 50-137, 141-243 초기 목록, 검색, 선택과 컷 추가 순서
  • Control/UC5.Designer.cs 해외지수 → 해외 업종지수 → 해외 종목의 세로 배치와 액션 버튼
  • Data/Request/봉차트/CandleforeignStock.cs 해외종목 봉차트의 이름·심볼·국가 조건
  • Data/Request/판데이터기본/종목/STOCK_FORGIEN_PAN.cs 해외종목 1열판의 정확한 종목 identity

고정한 계약:

  • 화면은 원본과 같은 세로 3그룹이며, 별도 action tree·상세 패널·선택 개수·국가 코드는 표시하지 않는다. 검색 Enter, 즉시 선택, 한 번의 Tab 진입과 ArrowUp/Down/Home/End 선택을 지원한다.
  • 빈 검색은 원본처럼 미국·대만 초기 목록만 조회하고, 검색어가 있으면 F_FDTC='1'인 모든 해외 국가를 이름으로 조회한다. DB 순서와 같은 이름의 서로 다른 종목을 그대로 보존한다.
  • 원본 FarPoint SheetViewActiveRowIndex=-1, RowCount=0에서 첫 AddRows 직후 활성행을 0으로 바꾼다. 고정 지수와 검색 결과의 첫 행을 같은 방식으로 자동 활성화해 별도 선행 클릭 없이 원본처럼 액션 버튼을 사용할 수 있다.
  • 원본에는 결과 상한이 없으므로 기존 100건 기본 상한을 데이터 경계의 안전 상한인 500건으로 넓혔다. 500건을 넘으면 조용히 숨기지 않고 목록 제목에 검색 결과가 많아 500건까지만 표시합니다.를 표시한다.
  • Web에는 generation에 묶인 opaque selection ID와 표시명만 전달한다. 심볼·국가·data code는 C# 권위 상태에만 보관하며 action intent에는 포함하지 않는다.
  • 플레이리스트 표시 열은 원본처럼 해외지수/해외업종/해외종목, 종목명, 1열판기본 또는 5/20/60/120/240일, 1/1만 사용한다.
  • 해외종목은 내부 symbol|nation|1 identity로 5001 봉차트와 8010 1열판에 전달해 같은 표시명도 정확한 심볼·국가로 조회한다. 기존 이름만 저장된 레거시 행은 미국·대만 name-only 조회와 계속 호환한다.
  • 현재 K3D 장면 경계가 처리하는 두 글자 국가 코드는 전 국가에 대해 실행 가능하다. 세 글자 국가 코드 결과는 목록에는 보존하되 컷 생성 단계에서 명시적으로 fail closed한다.
  • 동일 구조의 native 회신은 목록 DOM과 focus를 교체하지 않으며, 늦게 끝난 검색은 새로운 generation의 결과나 선택을 덮지 못한다.

명시적 안전 교정:

  • 원본의 이름 단독 장면 조회가 같은 이름의 여러 국가·심볼을 혼동할 수 있어, 신규 행은 정확한 내부 identity를 필수로 사용한다. 이 identity는 WebView 경계를 통과하지 않는다.
  • 지원하지 않는 국가 코드, 모호한 name-only 결과, 빈 선택은 PREPARE 전에 차단한다.

Slice 15: Release 패키지 컨텍스트와 실제 개발 환경 사전 검증

상태: Release x64 개발 등록 패키지의 UI-only/DryRun 검증, 실제 개발 DB SELECT-only 검증, 원본 Cuts 범위 검사와 K3D/Tornado 읽기 전용 사전 검사를 완료했다. COM/KTAP 송출 명령과 DB write는 실행하지 않았으며 검증 뒤 MBN_STOCK_WEBVIEW 프로세스는 남아 있지 않다.

검증 결과:

  • 패키지는 정확한 Wickedness.MBNStockWebView.LegacyParity_0.1.0.0_x64__qbv3jkvsn3aj0 Release x64 개발 등록에서 실행했다. URL은 https://legacy-parity.mbn.local/index.html, 상태는 DRY RUN/IDLE였고, guard 이후 native postMessage, mouse/keyboard 입력, DB read/write intent, 단일 인스턴스 재활성화와 Tornado 포트 연결은 모두 0건이었다. 중립 종료 대화상자의 기본 종료 동작을 정확히 1회 사용해 정상 종료했다.
  • 패키지 증거 JSON은 C:\Users\MD\source\repos\MBN_STOCK_WEBVIEW\artifacts\package-ui-only-smoke\legacy-package-ui-only-20260718T011058894Z.json (SHA-256 2AD2C06EB14DB9684548A5D086B9FA4EC53E504D6722400E1F16B6E600BCD341)이고, 화면 캡처는 같은 폴더의 legacy-package-ui-only-20260718T011058894Z.png (SHA-256 1CC02BEE872A15E64D4A74F8E489A212B4547464FB27784C4975E523B5132806)이다.
  • 생성된 MSIX는 C:\Users\MD\source\repos\MBN_STOCK_WEBVIEW\src\MBN_STOCK_WEBVIEW.LegacyParityApp\AppPackages\MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64_Test\MBN_STOCK_WEBVIEW.LegacyParityApp_0.1.0.0_x64.msix (SHA-256 3F68625C0D2ACBBFD673F2F2869944C5818A8732E94A91BA7E5C7F2707458228)이며, 금지된 외부 자산·비밀 값은 포함하지 않았다.
  • 실제 개발 Oracle/MariaDB에 SELECT-only smoke를 실행해 Oracle 21 연결, MariaDB 연결, 종목·해외 선택기, 전문가·거래정지·PList 조회를 확인했다. 필수 scene data 33/33, 전체 query 58개가 통과했고 DB write는 0건이다. 현재 NXT theme preview 48건이 빈 결과인 것은 실제 데이터 상태이며 코드 실패로 판정하지 않는다.
  • 원본 읽기 전용 Cuts의 reachable builder 34개와 active alias 45개를 검사했다. .t2s 누락·unsafe path·root escape는 모두 0건이다. 의존 자산은 23개 중 14개가 없으며, s5006 Video\큐브배경.vrv 1개와 s6001 Video\20201008_<13 countries>.vrv 13개 패턴이다. 따라서 5001은 실제 검증 후보로 유지하고 5006/6001은 자산 제공 전까지 차단한다.
  • K3D 등록은 HKLM Registry64, native AMD64 type library와 KAEngine/KAEventHandler 상호 매핑이 모두 유효했다. 현재 Tornado2 v2.7.5.49 x64 DSX10.0 S.R2의 PID 41308, 제목 PGM, listener 0.0.0.0:30001을 읽기 전용으로 확인했다. COM activation, CONNECT, PREPARE, TAKE IN, NEXT, TAKE OUT과 DISCONNECT는 실행하지 않았다.

남은 실제 검증:

  • 현재 PGM의 5001 page 1 검증은 이전 승인을 재사용하지 않고 새 회차·계획 해시로 승인받는다. 먼저 CONNECT/PREPARE의 known outcome과 Network Monitoring 기록을 확인하고, 그 뒤 TAKE IN/PGM 화면/ TAKE OUT은 허용 횟수를 명시한 별도 승인 범위에서만 실행한다. 결과가 불명확하면 재시도하지 않는다.
  • Network Monitoring 기록과 PGM 출력은 사용자가 함께 확인한 증거를 남긴다.
  • 5006/6001은 누락된 외부 동영상 자산이 안전하게 제공되고 coverage가 다시 통과하기 전에는 실제 검증하지 않는다.

다음 대조 순서

  1. 새 회차 승인으로 현재 PGM 대상 5001 page 1 CONNECT/PREPARE known outcome 검증
  2. Network Monitoring과 PGM 출력 확인 뒤 별도 승인 범위의 TAKE IN/TAKE OUT 검증
  3. s5006/s6001 외부 동영상 자산 보완과 Cuts coverage 재검증
  4. 전체 근거 검토 뒤 변경 사항 커밋·원격 반영

각 항목은 실제 원본 코드 근거와 회귀 테스트가 모두 생기기 전에는 완료로 판정하지 않는다.

2026-07-20 실제 사용자 입력 회귀 보정

상태: 보고된 세 가지 조작 문제를 보정하고 Release x64 개발 등록 패키지의 실제 Windows mouse/keyboard 입력으로 DryRun 종단 검증을 완료했다. Tornado 연결과 DB write는 실행하지 않았다.

  • 원본의 NEXT 기준은 임의로 새로 클릭한 행이 아니라 마지막 TAKE IN으로 PROGRAM이 된 행이다. 빠른 행 클릭 -> TAKE IN이 앞선 행 선택 처리와 겹치면 TAKE IN이 유실될 수 있던 native intent admission을 순서 보존 방식으로 보정했다. 실제 입력에서 다른 행을 먼저 후보로 만든 뒤 목표 행을 클릭하고 즉시 TAKE IN했으며, 목표 행 PROGRAM 후 정확한 다음 enabled 행으로 NEXT됨을 확인했다.
  • 좌측 컷 drag는 기존 captured-pointer 임계값과 drop 계약을 유지하면서 마우스를 따라가는 추가 가능/불가 feedback을 표시한다. threshold 전에는 숨김, 목록 밖에서는 불가, 편성표 위에서는 가능, release/cancel/Escape에서는 즉시 제거되는 것을 실제 입력으로 확인했다.
  • PREPARED/PROGRAM의 안정 상태에서도 좌측 컷의 순수 선택과 focus는 허용한다. 이때 pointer intent는 allowAppend=false로 닫혀 있어 빠른 세 번 클릭과 double-click도 편성표 행 수를 바꾸지 않는다.
  • 첫 검증 fixture의 다음 행은 장중 데이터가 없는 시간외단일가여서 원본과 동일하게 장면 생성 전에 invalid row count로 안전 차단됐다. 이는 NEXT 누락이나 결과불명이 아니므로, 검증 도구는 native error를 즉시 known rejection으로 판정하고 NEXT 행 이동 검증에는 서로 다른 두 현재가 행 ID를 사용하도록 수정했다.
  • 10초 DB 상태 점검은 사용자/송출 DB 작업의 priority waiter를 gate 획득 전 게시하고, health CTS도 Interlocked.Exchange로 게시한다. 상태 점검의 gate 획득과 cancellation 게시 사이에 입력이 와도 probe가 양보하도록 경쟁 구간을 닫았다.

실제 패키지 증거:

  • JSON: artifacts/package-input-smoke/legacy-package-input-20260720T032627652Z.json (SHA-256 9A5F68C58C875A308360D8571ECE7D2B59AF8D258C8F3A3DADC134B1C4F596FE)
  • 화면: artifacts/package-input-smoke/legacy-package-input-20260720T032627652Z.png (SHA-256 EADF727632DF39F6509D484B613A63A4595BD56BCB5A868C3D8FE38E73DC8DC7)
  • 결과: PASS, DryRun playout PASS, playlist count 3 -> 3, blocked outbound 0, state violation 0, DB write 0, 앱 PID의 Tornado :30001 연결 0, 정상 종료 및 port 해제 확인.

당시 회귀 결과(최신 기준선은 MIGRATION_STATUS.md 참조):

  • JavaScript: 502/502
  • .NET solution: 2817/2817
  • LegacyWeb native contract: 117/117
  • Release x64 unsigned 개발 패키지 빌드: 경고 0, 오류 0

정정: 위 Slice 15에 남은 회차별 신규 승인 문구는 이후 기록된 개발 Tornado2/PGM 상시 권한으로 대체됐다. 현재 권한과 stop condition은 저장소 AGENTS.md가 권위 기준이다.