# 원본 Tornado 송출 흐름 분석

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

이 문서는 `C:\Users\MD\source\repos\MBN_STOCK_N`의 `MainForm`, `Scene` 35개와 `PageN`/`Nxt_PageN`을 새 송출 runtime과 대조한 기준선이다. 원본은 읽기 전용으로만 조사했으며 소스, DB 비밀번호, 운영 설정과 실제 자산을 새 저장소로 복사하지 않았다.

## MainForm 호출 순서

원본 연결은 UI STA에서 `KTAPConnect(1, "127.0.0.1", 30001, 0, event)`를 호출한 뒤 `GetScenePlayer()`를 얻는다. 원본의 `30001`은 당시 운영값일 뿐 새 Test endpoint의 기본값이 아니다. 새 회차에서는 Tornado2 `Tools > Option > Control > Network Server > TCP Port`의 실값을 사용하고, 반환값 `1`뿐 아니라 `OnHello`와 Network Monitoring `[R]`/`[S]`를 함께 확인해야 한다.

원본 PREPARE의 순서는 다음과 같다.

1. `LoadScene(Cuts\<file>.t2s, <scene alias>)`
2. IN effect flag `1`에 fade effect `7`과 `FadeInSec` 적용
3. `BeginTransaction()`
4. 장면별 object 값과 시각 속성 변경
5. `scene.QueryVariables()`
6. `EndTransaction()`
7. `player.Prepare(10, scene)`

새 `DynamicK3dSession`도 같은 순서를 사용한다. output channel이 명시된 경우에만 `EndTransactionOnChannel`을 사용하며 layout `10`은 `Prepare`/`Play`/`CutOut`에만 전달한다. transaction 중 실패하면 `RollbackTransaction`을 시도하고 원래 실패를 보존한다.

공통 scene background 경로는 Web 입력이 아니라 로컬 trusted 설정과 네이티브 파일 선택에서만 온다. fade는 원본 `DissolveTime`/`ComboDi.SelectedIndex`와 같은 0~19 폐쇄형 숫자 intent이며 기본 인덱스는 6(화면 표시 7)이다. K3D 호출도 원본의 raw Int32를 보존해 `SetSceneEffectType(10, FADE, index)`와 공통 `SetBackgroundVideo(path, 2004, 10)`을 사용하고, s5006 내부 배경의 `(1, 1)`과 섞지 않는다. background kind와 scene root 아래 상대 asset, video loop를 `PlayoutSceneCompositionFactory`가 검사하며 `DryRun`도 실제 COM 전에 파일 존재, 확장자, root 탈출과 reparse point를 fail-closed 검증한다. 실제 asset 경로는 Web preview나 wire status에 노출하지 않는다.

## PREPARE, TAKE IN, NEXT, TAKE OUT 상태 전이

- PREPARE는 선택 위치부터 다음 활성 row를 찾아 page 0의 실제 데이터를 조회하고 scene을 load/transaction/prepare한다. 성공한 전체 playlist와 선택 index는 immutable native snapshot으로 고정된다.
- PREPARE/PROGRAM이 이미 활성인 상태에서 PREPARE를 다시 누르면 원본 toggle과 같이 `TakeOut(All)`/`StopAll`로 정리한다.
- TAKE IN은 PREPARE 때의 DTO를 그대로 play하지 않는다. 원본이 `m_Super`를 초기화하고 `ONAirMode`를 다시 호출하는 것처럼 같은 frozen entry/page를 DB에서 새로 조회하고 `LoadScene` → transaction → `Prepare(10)`한 뒤 성공한 경우에만 `Play(10)`한다.
- NEXT는 on-air 상태에서만 허용한다. 다음 page가 있으면 Page NEXT, 마지막 page면 다음 활성 playlist entry의 page 0으로 구분한다. 끝에서 wrap하지 않는다.
- TAKE OUT은 현재 원본 운영 경로와 같이 `StopAll()`을 사용하고 on-air 상태를 해제한다.

timeout, dispatch 뒤 cancellation 또는 결과가 불명확한 COM 실패는 `OutcomeUnknown` latch로 남긴다. 취소나 retry 가능한 실패로 낮추거나 같은 명령을 다시 보내지 않는다.

## PageN, Page NEXT와 timer refresh

`PageN`과 `Nxt_PageN`은 조회 행 수를 5·6·12개 단위로 나눠 최대 20페이지의 `m_pcnt`를 계산한다. 새 구현의 대상은 `s5074`(5), `s5077`(6), `s5088`(12)이며 `pageCount = min(20, ceil(itemCount/pageSize))`를 사용한다.

원본에는 같은 scene을 바꾸는 두 경로가 있다. 서로 혼동하면 안 된다.

1. Operator Page NEXT는 `btnNext_Click` → `Next_Scene(0)` 경로다. 다음 page의 fresh 데이터를 조회하고 새 scene을 `LoadScene` → transaction → `QueryVariables` → `EndTransaction` → `Prepare(10)` → `Play(10)`한다. playlist index는 유지하지만 `GetPlayingScene` in-place 갱신은 아니다.
2. Timer refresh는 `timer1_Tick` → `Show_PlayList(idx: 1)` 경로다. current entry/page의 fresh DB DTO를 사용하고 원본 K3D 호출에는 mutation 전 선행 `Play(10)`과 `GetPlayingScene(10)` → transaction → `QueryVariables` → `EndTransaction` → `Prepare(10)` 뒤 후행 `Play(10)`이 모두 있다. scene-level background와 transition effect는 다시 적용하지 않는다.

operator command를 시작할 때 timer를 먼저 멈춘다. TAKE IN과 playlist NEXT 성공 뒤 반복 refresh를 시작하고 Page NEXT 뒤에는 원본처럼 다시 시작하지 않는다. 새 runtime은 이전 tracked PLAY의 callback drain을 먼저 확인한 뒤 해당 cut의 첫 `m_time`, 이후 3초의 전체 cooldown을 시작한다. 원본의 UI timer보다 실제 cadence가 callback drain만큼 길어지는 의도적 안전 적응이며, callback 대기와 delay를 겹쳐 `playCompletionPending`이 사실상 계속 유지되는 상태를 방지한다.

단, 5076/5079/5080/5081의 `m_time=3000000`은 원본에서 `m_time * 1000` Int32가 overflow되어 WinForms `Timer.Interval` 설정이 예외로 끝나므로 자동 refresh를 시작하지 않는다. 이를 34.7일짜리 정상 타이머로 해석하지 않는다.

마이그레이션의 same-scene refresh는 retained on-air scene에 fresh DTO를 적용해 `BeginTransaction → mutation → QueryVariables → EndTransaction → Prepare(10) → tracked Play(10)`을 한 번 수행한다. 원본의 선행·후행 PLAY 두 번 중 선행 replay와 불완전한 `GetPlayingScene` proxy를 사용하지 않는 명시적 안전 차이다. 모든 PLAY를 callback accounting과 연결하고 중복 송출을 피하기 위한 결정이며, Round H의 정확한 `PLAY = 4`, `SCENE_PLAYED = 4` 예산으로 검증했다. refresh generation과 공개 상태는 atomic epoch로 묶어 이전 CTS의 늦은 delay, success, fault 또는 completion이 새 generation이나 stopped 상태를 덮어쓰지 못한다.

마지막 부분 page는 남은 row만 채우고 나머지 object를 clear/hide한다. `s5088` NXT 비교와 조회 index는 모두 `i + pageIndex * 12`를 사용한다. page 경계값과 partial-page clearing은 자동 테스트로 검증했다.

## 35개 Scene builder와 실제 데이터

원본 `Scene` 폴더의 35개 builder는 모두 typed DTO와 COM-neutral mutation builder로 포팅했다. 값, visibility, face color, position, position key, scale, crop key, circle angle, path/path-shape, image/texture/video와 scene background를 개별 mutation으로 표현하고 `IPlayoutEngine` 뒤에서만 K3D 호출로 변환한다.

- registry/catalog: 35개 builder 1:1
- MainForm 도달 runtime: 34개 builder, active cut alias 45개
- `s5032`/`s8018`: `5032`, `8018`, `8032` shared alias를 closed selection으로 분기
- `s8086`: 원본 MainForm dispatch가 없어 active alias와 앱 runtime route 없이 diagnostic으로 유지
- 실제 데이터 smoke: 33개 Oracle/MariaDB loader와 `s5025` trusted 외부 CP949 파일 전제조건을 포함해 통과. 해당 trusted 파일은 Git/MSIX 밖에서 별도로 제공해야 한다.
- `s8086` diagnostic 조회 통과, 전체 Oracle/MariaDB query 55건 통과

builder별 object/mutation과 검증 상태는 [`SCENE_EQUIVALENCE.md`](SCENE_EQUIVALENCE.md)에 있다. 실제 `.t2s`, DB 계정과 외부 CP949 파일은 Git에 넣지 않는다.

## WebView 상태와 안전 경계

Web catalog는 35개 builder와 도달 가능한 45개 alias를 제공하고 playlist row별 `enabled` flag를 보존한다. PREPARE가 성공하면 native snapshot을 freeze하며 pending command, `OutcomeUnknown`과 timeout quarantine 중에도 편집 잠금을 유지하므로 이후 Web 편집으로 NEXT 대상을 바꿀 수 없다.

native status는 현재 entry, builder, page size/index/count, current row 수, last-page, next kind와 bounded typed preview를 authoritative 값으로 보낸다. preview는 object 값과 상태를 보여주되 asset path는 숨긴다. refresh active/next/last-success/fault도 표시한다. 성공한 TAKE OUT으로 native refresh state가 reset되면 전용 refresh error marker만 제거하고 다른 unknown/quarantine latch는 유지한다.

refresh CTS와 상태는 같은 atomic epoch 안에서 교체·중단·조건부 갱신·완료한다. 따라서 중단된 이전 loop가 `refreshActive`를 다시 켜거나 오래된 `NextAt`, success, fault를 새 scene 상태 위에 기록할 수 없다. NEXT는 epoch를 먼저 중단하고 pending PLAY callback이 있으면 DB나 workflow에 들어가기 전에 즉시 fail-closed 반환하므로 TAKE OUT을 긴 callback 대기로 막지 않는다.

Web 응답 제한 시간이 지나면 strict `ParseTimeoutQuarantine` 요청을 native에 보내고 MainWindow가 process-lifetime correlation latch를 먼저 세운 뒤 vendor session을 quarantine한다. 명령 진행 중 trusted navigation, reload 또는 WebView2 process failure도 JavaScript 상관관계를 잃기 전에 같은 latch를 세우므로 WebView reload로 해제되지 않는다. pending request와 맞지 않는 늦은 응답은 state 전이 근거로 쓰지 않으며 같은 명령을 다시 보내지 않는다. `OutcomeUnknown`, native fault와 Web timeout은 UI를 닫는 것으로 해제되지 않는다.

## Callback과 scene 수명

vendor event handler의 `OnScenePlayed`, `OnCutOut`, `OnStopAll`은 managed callback queue로 연결돼 있다.

- `OnScenePlayed` 성공 뒤에만 이전 retired scene을 unload/release한다.
- pending Play callback이 있으면 TAKE OUT을 제외한 PREPARE/TAKE IN/NEXT/timer refresh를 fail-closed 차단한다.
- pending CutOut/StopAll callback이 있으면 중복 TAKE OUT을 포함한 모든 scene 명령을 차단하고, 일치하는 완료 callback을 drain한 뒤에만 다음 PREPARE를 허용한다.
- `CutOut`/`StopAll` dispatch 전에 completion counter를 올리고 동기 호출 실패 시 원복한다. 성공 callback은 대응 counter를 하나 줄이고, stop/cut으로 중단된 Play는 별도 `OnScenePlayed`가 없을 수 있으므로 pending Play accounting을 취소한다.
- pending lifecycle callback, queue overflow, callback failure 또는 connection generation 불일치가 있으면 Disconnect와 조기 unload를 하지 않고 session을 abandon/quarantine한다.
- 이전 generation의 늦은 callback은 현재 scene state를 변경하지 않는다.

따라서 장기 실행 시에도 callback으로 안전성이 확인된 retired scene만 unload된다.

## 검증 판정 범위

다음 자동·통합 검증은 clean `main`의 `fa0eae7b61f57f134dfd30ebf1e59e9cc2cccfc5`에서 완료됐다.

- 35개 builder, loader, resolver, runtime coverage와 PageN 경계
- 실제 Oracle/MariaDB 및 trusted CP949 source → DTO → mutation preflight
- Debug/Release x64 Core 790개, Playout 374개, Infrastructure 65개와 Web safety suite
- 실제 Oracle/MariaDB read-only query 55/55
- Visual Studio 2026 MSBuild `18.7.8` Debug/Release x64 및 앱 패키지 빌드
- 서명·설치·내용 감사를 통과한 Release x64 MSIX(SHA-256 `D51E8CB637860D9F0377AEE6FF691D7D954BA0FF7E30B1AB65443DA153BC5429`)와 packaged `DryRun`

실제 Tornado2 검증은 계획 SHA-256 `55C7755C2F874B4B012239117D4AB717BBCE194E7DCC8DA9B0E40D094FF8CA19`의 `MBNWEB-20260711-H`에서 승인된 `5001`과 `5074`만 실행했다. PGM에서 5001, 5074 page 1/page 2를 확인하고 TAKE OUT 뒤 검은 화면을 확인했다. Network Monitoring은 `SCENE_PREPARE 5001 = 3`, `SCENE_PREPARE 5074 = 2`, `PLAY = 4`, `SCENE_PLAYED = 4`, `FAILURE = 0`, `ERROR = 0`, retry 0을 기록했고 최종 `OutcomeUnknown`은 false였다.

이 실제 증거는 두 scene에만 적용한다. 나머지를 포함한 35개 builder/45개 alias와 PageN 경계는 자동 매트릭스 검증의 범위이며, 다른 scene을 PGM에 보내려면 자산과 명령 예산을 고정한 새 승인 회차가 필요하다. 실제 운영 검증과 복구는 [`PLAYOUT_OPERATIONS.md`](PLAYOUT_OPERATIONS.md)의 반복 금지 절차를 따른다.
