# MBN_STOCK_N 동작 동등성 작업 기록

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

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

기준 원본은 `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.cs`의 `SetSceneEffectType(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#의 `FirstText`와
  `SecondText`가 권위 상태를 가진다. 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#이 로컬
  시각 0~12시에는 장전, 13~23시에는 장후 세션을 탭 진입과 검색 시점마다 결정한다.
- 화면은 검색 입력, 테마 결과, 정렬 라디오, `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 `SheetView`는 `ActiveRowIndex=-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`가 권위 기준이다.
