Files
MBN_STOCK_WEBVIEW/docs/PLAYOUT_OPERATIONS.md

43 KiB

MBN_STOCK_WEBVIEW 송출 운영·검증 절차

이 문서는 마이그레이션된 WebView → IPlayoutEngine → K3D/Tornado2 경로를 검증할 때의 승인, 실행, 감시, 장애 복구와 rollback 기준이다. 기존 PLAYOUT.md의 K3D 설치·해시 핀·격리 진단 규칙을 대체하지 않고, 35개 scene과 PageN 동등성 검증에 필요한 운영 절차를 추가한다. 화면/계약 구현과 운영 동등성의 최신 재감사 상태는 LEGACY_FEATURE_AUDIT.md를 따른다.

현재 상태

항목 상태
기본 모드 DryRun; COM 객체와 KTAPConnect를 만들지 않음
자동 테스트 최종 전체 재검증에서 Core Debug/Release x64 각각 1,383/1,383, Playout 각각 393/393, Infrastructure 각각 173/173(합계 각 1,949/1,949), Web 415/415 통과; 실패·skip 0건
Visual Studio 2026 MSBuild 18.7.8.30822 Debug x64와 signed Release x64 패키지 빌드 성공
trusted Release x64 MSIX clean commit df60e09에서 1.0.5.0 생성·CN=Comtrophy 서명/SignTool 검증·x64 update 설치·package context 실행 성공. SHA-256 1B0BA713619CC3BD7CB3E03D78FD1EB1CEAF49E7D50F5C00DDFB483917BA5437; 290개 항목, source Web 33개 byte 일치, 금지 자산 0건. 실제 삼성전자 현재가 5001 DryRun에서 자동 refresh 1회 뒤 CAPPED 1/1, 3.5초 무변화, TAKE OUT/IDLE, Oracle/MariaDB Healthy, KTAP 미호출을 확인
실제 DB read→DTO→mutation smoke 34개 도달 가능 data route 통과: 33개 DB + s5025 trusted 외부 CP949 파일. s8086 diagnostic과 NXT restore audit를 포함한 Oracle/MariaDB query 58건 통과. 종속 asset·운영 DB-W·PGM 검증은 포함하지 않음
s5025 trusted 수동 파일 외부 CP949 source 통합 검증 통과; 실제 파일/환경은 계속 Git 밖에서 회차별 preflight
s5006 Video\큐브배경.vrv 현재 승인 Cuts root에서 누락
s6001 해외지수 영상 국가별 Video\20201008_<국가>.vrv 13개가 현재 승인 Cuts root에서 누락; 외부자산 필요
이번 마이그레이션 WebView workflow의 실제 Tornado2 검증 최신 성공은 승인된 5001/5074 범위의 Round H. Round J/K/L은 각각 pre-dispatch 명령 0건으로 종료됐고 창 preflight를 교정한 Round M은 Gate A 승인 대기; 35개 전체의 실제 PGM 검증으로 확대 해석하지 않음

1.0.2 Visual Studio 개발 등록본을 signed 1.0.3으로 교체할 때 Remove-AppxPackage -PreserveApplicationData를 사용했다. 재등록 구간에서 바뀐 파일은 Windows package Settings transaction log settings.dat.LOG1/LOG2뿐이었고, WebView LocalState와 외부 %LOCALAPPDATA%\MBN_STOCK_WEBVIEW 운영 데이터는 삭제·덮어쓰지 않았다. 이 패키지 검증 회차는 기본 설정 부재와 환경 override 0건을 확인한 DryRun으로만 실행했으며 Tornado 실제 명령은 보내지 않았다.

1.0.3에서 1.0.4로는 앱 종료 확인창을 통한 정상 종료 뒤 같은 package family에 in-place update했다. remove/reset 없이 비교 8쌍과 one-time marker가 유지됐고, 설치본에서 실제 삼성 63건 검색과 10개 업무 탭 상태 유지, 단일 인스턴스를 확인했다. 로그는 DryRunReady와 DB Healthy만 기록했으며 playout command는 0건이다. 상세 내용은 RELEASE_1_0_4_AUDIT.md에 있다.

1.0.4에서 1.0.5로도 정상 종료 뒤 같은 package family에 in-place update했다. 새 signed package에서 trusted 자동 refresh 상한 1을 package-context DryRun으로 검증하고 정상 종료 뒤 임시 local config와 모든 playout 환경 override를 제거했다. 상세 내용은 RELEASE_1_0_5_AUDIT.md에 있다.

과거 고정 5001 → 5006 runner의 실제 PGM 기록은 연결과 기본 K3D 명령 표면의 증거다. 현재 WebView playlist, fresh TAKE IN, Page NEXT, timer refresh, callback/unload의 동등성 완료 증거는 아니다.

2026-07-11의 승인 회차 MBNWEB-20260711-A에서는 현재 MSIX WebView runtime으로 CONNECT → PREPARE 5001/page 1까지 성공했다. Network Monitoring에 HELLO, LOAD_SCENE, BEGIN_TRANSACTION, 데이터 mutation, QUERY_VARIABLES, END_TRANSACTION, SCENE_PREPARE의 request/success가 기록됐고 PREPARE 동안 PGM은 검은 화면이었다. TAKE IN은 두 번째 K3D Prepare/Play 전에 수행하는 fresh Oracle 조회에서 DATABASE_UNAVAILABLE로 명확히 실패했다. PLAY가 없고 PGM도 검은 상태임을 확인해 TAKE IN을 반복하지 않았으며, 승인 범위의 TAKE OUT All 한 번으로 STOPAL → UNLOAD_SCENE 5001 성공을 확인하고 BYE 뒤 앱을 정상 종료했다. 직후 probe는 Oracle unhealthy/MariaDB healthy와 정상 DNS/TCP 도달을 보였고, 회차 종료 뒤 전체 55-query 스모크는 두 DB 모두 다시 통과했다. 이 회차는 실패·복구 증거이며 동등성 완료 증거가 아니다. 새 package/계획 SHA-256과 새 Gate A/B로 처음부터 다시 시작한다.

이 회차 뒤 health query와 시장 탭 DB 조회를 송출 fresh query와 하나의 DB activity gate로 직렬화했다. 송출 명령과 자동 on-air refresh가 시작되면 진행 중인 health/market 조회를 취소하고 gate를 먼저 확보하며, 30초 periodic health는 송출 중 건너뛴다. native database status에는 단조 증가 sequence를 붙이고 Web은 오래된 응답을 폐기한다. DB health의 IsTransient와 숫자 provider error code만 진단 도구에 보존하며 SQL, parameter, host, 계정, 암호, connection string과 provider message는 출력하지 않는다.

Round G 실패 경계와 Round H 최종 실제 검증

Round G MBNWEB-20260711-G는 5001 TAKE IN과 화면 표시까지 성공했지만, 승인된 자동 refresh 1회 예산을 넘어 refresh가 연속 실행되는 경계에서 중단됐다. Network Monitoring에는 성공한 transaction/prepare 39회와 PLAY/OnScenePlayed 38회가 기록됐고 FAILURE/ERROR는 각각 0이었다. refreshLastSuccessAt은 전진했지만 playCompletionPending이 사실상 계속 유지되어 관찰 대기가 timeout됐으며, 5074 playlist NEXT와 Page NEXT는 보내지 않았다. 출력이 5001로 명확히 active이고 OutcomeUnknown=false였으므로 승인된 비상 TAKE OUT을 정확히 한 번 보내 STOPALL과 5001 UNLOAD, PGM black, disconnect를 확인했고 재시도는 0회였다.

Round G 증거 경계는 다음 SHA-256으로 고정한다.

증거 SHA-256
계획 E8A6B05E28130665A73CC882A70CD85F023EBF6C59795EC3A56671BE06B06B9A
회차 결과 9B573EBB0F5465164298CA96099E1853DE6E6C00B3D375FB81FC954BED3464A0
manifest A0AA8E95CB7F0BE52E21438094D1FFCDA53245C9E33CDC5A659D513EEF19D1EC

Round H MBNWEB-20260711-H는 이 실패 경계를 수정한 source commit fa0eae7b61f57f134dfd30ebf1e59e9cc2cccfc5(fa0eae7)와 다음 불변 입력으로 실행했다.

항목
계획 SHA-256 55C7755C2F874B4B012239117D4AB717BBCE194E7DCC8DA9B0E40D094FF8CA19
회차 결과 SHA-256 AB70E61C86909F16FE407AAEEF0A5E668F8A4614375448E860C7BE47D687FA0D
증적 manifest SHA-256 CD4EFD0CF64D9C905D7ABF732296E5A80DD4DE362DC7C88FD0D6967846955494
서명된 Release x64 MSIX SHA-256 D51E8CB637860D9F0377AEE6FF691D7D954BA0FF7E30B1AB65443DA153BC5429
명령 예산 CONNECT 1, PREPARE 1, TAKE IN 1, 5001 자동 refresh 1, 5074 playlist NEXT 1, 5074 Page NEXT 1, TAKE OUT 1, disconnect 1, retry 0

실제 PGM에서는 5001 삼성전자 화면, 5074 page 1과 page 2를 차례로 확인했고 TAKE OUT 뒤 black을 확인했다. Network Monitoring의 최종 증분은 다음과 일치했다.

명령군 request/success 또는 callback
HELLO 1/1
LOAD 5001 / LOAD 5074 각각 1/1
PREPARE 5001 / PREPARE 5074 각각 3/3, 2/2
PLAY / OnScenePlayed 4/4, callback 4
STOPALL 1/1
UNLOAD 5001 / UNLOAD 5074 각각 1/1
FAILURE / ERROR 0/0
BYE request 1, 별도 success marker 0

BYE success marker가 0인 것은 계획의 허용 범위 0~1 안이며, 앱의 disconnected terminal state와 callback drain, 최종 PGM black을 함께 확인했으므로 정상 종료로 판정했다. 전 과정은 OutcomeUnknown=false, retry 0이었고 실패 뒤 추정 명령이나 반대 명령을 보내는 rollback은 필요하지 않았다. 정상 cleanup으로 승인된 TAKE OUT 1회, 두 scene unload, disconnect, 앱과 진단 listener 종료, 회차 전용 Live 설정·승인 환경 제거, PGM/Network Monitoring 창 상태 복원을 확인했다. 이후 기본 모드는 다시 DryRun이며 장애 rollback은 Disabled 전환 또는 직전 승인 패키지 복귀만 사용하고 vendor 설치·등록·라이선스는 변경하지 않는다.

Round H 전에 Core 790, Infrastructure 65, Playout 374개 테스트를 Debug/Release x64에서 각각 모두 통과했고, 실제 Oracle/MariaDB read-only query 55건, Visual Studio 2026 Debug/Release x64와 package build, 서명·설치·package context 검사를 통과했다. 같은 signed MSIX의 package-context DryRun도 5001 PREPARE → TAKE IN → 자동 refresh 정확히 1회 → 5074 page 1 → page 2 → TAKE OUT과 최종 IDLE, OutcomeUnknown=false, KTAP 미호출을 확인했다.

Round I 회귀 실패와 native 자동 갱신 상한

Round I MBNWEB-20260712-I는 계획 SHA-256 D221841A15593A987FB944648F0ACE0AC5940936AEE16C74DC9B901A2F45B332로 CONNECT 1회와 PREPARE 5001/page 1, TAKE IN 1회를 실행했다. 5001 삼성전자 PGM 표시는 확인했지만, 실시간 관찰 자동화가 TAKE OUT까지 하나의 중단 없는 작업으로 묶이지 않아 승인된 자동 refresh 최대 1회를 넘어 9회가 실행됐다. 최종 누적 PLAY/OnScenePlayed는 각각 10회(초기 TAKE IN 1 + refresh 9)였다. NEXT와 다른 scene LOAD는 없었고 FAILURE/ERROR는 0이었다. 승인된 TAKE OUT 1회로 STOPALL/UNLOAD 5001 각 1회 성공, PGM black, BYE request 1, 앱 정상 종료를 확인했다. 회차 결과는 FAIL, OutcomeUnknown=false, vendor command retry 0이며 결과 SHA-256은 E5FD13E7FCDF87B6ADCFB696EFF2E957B70ECA821A424FC15EE41043CFA0E37E이다.

로컬 결과 파일은 artifacts\pgm-evidence\MBNWEB-20260712-I\round-result.json이고, 적용한 Live 설정 SHA-256은 BE282B9AF0AC5D8C243234D70587497ADACC7A47ED85C1865ED595949FC8759F다. 운영 증거 원본은 Git 제외 대상이므로 Gitea에서는 결과 hash만으로 파일을 재취득할 수 없다. 또한 Round I 계획은 자동 refresh 1회를 허용하면서 일부 예상 native range에는 그 추가 PREPARE/PLAY를 반영하지 않은 불일치가 있었다. 새 계획은 Gate A 뒤 누적 PREPARE 1/PLAY 0, Gate B cap 도달 뒤 누적 PREPARE 3/PLAY 2/ OnScenePlayed 2를 명시해야 한다. 종료 직전 native snapshot이 별도 파일로 남지 않은 점도 Round I의 증거 제한으로 보존한다.

이 실패 뒤 maximumAutomaticRefreshesPerTakeIn trusted JSON-only 상한을 추가했다. 기본 null은 원본 연속 갱신을 유지하고, 실제 검증 설정의 1은 scheduler와 실제 refresh dispatch 경계에서 두 번째 실행을 이중 차단한다. 상태는 refreshCompletedCount, refreshMaximumCount, refreshLimitReached를 제공하며, 마지막 refresh OnScenePlayed가 drain되기 전에는 count가 상한에 도달해도 refreshLimitReached=false를 유지한다. 검증은 completed=1, maximum=1, limitReached=true, refreshActive=false, playCompletionPending=false를 모두 확인해야 한다. 변경된 package는 Round I 승인/계획을 재사용할 수 없으며 새 package hash, 새 회차 계획과 새 CONNECT/PREPARE/TAKE IN 승인이 필요하다.

Round J/K/L pre-dispatch 실패와 Round M 교정

Round J MBNWEB-20260712-J는 계획 SHA-256 8B51E43AFC5F0E330A8DEF3B38DF071B071E5814FC96258AA9505935D1B43956에 대한 Gate A 승인을 정확히 한 번 사용했지만, Windows PowerShell 5.1에서 승인 나이를 System.DateTime - System.DateTimeOffset으로 계산하면서 execution marker 전에 실패했다. 결과는 FAIL_KNOWN_CLEANED, OutcomeUnknown=false다. 앱 launch, CONNECT, PREPARE, TAKE IN, NEXT, Page NEXT, DB write, TAKE OUT, DISCONNECT와 retry는 모두 0건이다. Gate A 결과 SHA-256은 BC6FBD867A7BAC01E5785AD6A3B488709FDBC5E778BDF4479D35CA73A02C19F1이다. 비명령 cleanup은 성공했고 앱/CDP/설정/capability/lease가 남지 않았다. J 승인과 명령 예산은 폐기하며 같은 계획으로 재시도하지 않는다.

Round K MBNWEB-20260712-K는 별도 회차이며 계획 SHA-256은 8B25D2E9987AECF90526752CD9CF9BAB769D2DA4F689064334BE4039FC2E0BC6이다. 승인 나이는 [DateTimeOffset]::UtcNow와 UTC DateTimeOffset끼리 계산하고, Math.Max에는 double 인수를 명시해 소수초를 보존한다. Windows PowerShell 5.1 회귀시험은 +5.000초 허용/+5.001초 거부, 900.000초 허용/900.001초 거부와 60.125초 보존을 확인한다. Gate A는 승인 파일 생성 뒤 정확히 계획·승인· Live 설정·installed manifest·계획에 SHA로 묶인 helper 7개만 있는지 계획 해석 전과 capability 생성 직전에 검사한다. 폴더, reparse, 미지·임시·partial·static-test 파일, 누락과 중복은 모두 fail-closed다.

K Gate A는 승인 시간 계산을 통과했지만 설치 runtime manifest의 culture sort가 strict ordinal 검사를 만족하지 않아 execution marker 전에 FAIL_KNOWN_CLEANED로 끝났다. manifest 289개 항목과 내용은 정상이지만 ordinal 역전이 41개였고 첫 역전은 clrjit.dll → Config/appsettings.example.json이다. 앱 launch/CONNECT/PREPARE와 금지 명령은 모두 0건이며 OutcomeUnknown=false다. 결과 SHA-256은 E17FE1EE451A5D9E2B73CCB3D039B5AA90998CC35E3E973148614D32E76D1482다. K 승인과 예산은 폐기하며 재사용하지 않는다.

Round L은 289개 path/length/SHA-256을 그대로 두고 [StringComparer]::Ordinal로만 재정렬했다. manifest SHA-256은 9631E8188E10F23970F43593B705957A215AE01E2760FE714B08518DB0571A9B이며 역전 0, K/L 의미 집합 289/289 동일, ko-KR/en-US/tr-TR 불변성을 확인했다. Gate A/B 모두 exact schema, NFC와 안전 경로, exact/대소문자 충돌, strict ordinal 순서를 검증한다. Node 계획 생성기의 18자리 start ticks 반올림도 승인 전에 발견해 Int64 보존 회귀를 추가했다. 최종 L 계획 SHA-256은 23E3A6A5400166A47835E146DCC2D19C181E2100D1485DB428F1B10718D89928이다. 승인 전 운영 폴더는 정확히 10개 파일이며 앱/Tornado 명령은 0건이다. Gate A 예산은 CONNECT 1회, PREPARE 5001/page 1 1회, 명확한 known-outcome 중단의 DISCONNECT 최대 1회다. TAKE IN, NEXT, Page NEXT, DB write와 retry는 0이다. 세부 계획은 LIVE_VALIDATION_PLAN_MBNWEB-20260712-L.md에 고정했다.

승인된 Round L Gate A는 manifest와 시간·Int64 검사를 통과했지만 frozen PGM handle 592052가 visible=true, iconic=true여서 strict desktop target 검사에서 execution marker 전에 종료됐다. 결과는 FAIL_KNOWN_CLEANED, outcomeUnknown=false이며 앱 launch, CONNECT, PREPARE, TAKE IN, NEXT, DB write, DISCONNECT와 retry는 모두 0건이다. Gate A 결과 SHA-256은 F57BEA9E5F4A215DBB944DCFDC22D8989A5418BDC3910B19B2A11ED9B795FE4D다. L 승인과 예산은 폐기했고 재사용하지 않는다.

새 Round M은 PID/start/exe/signature/listener와 유일한 exact 창 identity를 먼저 검증한 뒤, 필요한 창에만 동기 ShowWindow를 창별 최대 1회 적용한다. 성공 판정은 API 반환값이 아니라 5초 이내 visible/non-iconic/non-cloaked 사후 상태다. 두 창의 원본 placement는 첫 변경 전에 메모리에 보존하며 helper 내부 부분 실패, execution marker 전 Gate A 실패와 marker 이후 실패 모두 exact 원상복구 경로가 있다. preflight 동안 파일, capability, 앱 명령과 벤더 명령은 0건이다. schema 4 계획은 19,042 bytes, SHA-256 7A1141C5F276729E340227CEEAA48B0E6018632BBA51F43AA94636084AA3C301이며 승인 전 실제 명령은 0건이다. 세부 계획과 정확한 승인문은 LIVE_VALIDATION_PLAN_MBNWEB-20260712-M.md에 고정했다.

이 실제 Live PGM 증거는 회차에 허용된 5001과 5074에 한정된다. 35개 scene builder 전체 동등성은 자동 테스트, 55-query 실데이터 smoke와 SCENE_EQUIVALENCE.md 매트릭스로 유지하며, 나머지 scene이 실제 PGM에서 각각 송출됐다는 의미로 기록하지 않는다.

화면/계약과 운영 검증의 분리

  • 화면·native bridge·DTO·mutation·identity 상관 계약이 존재하고 자동 테스트가 통과한 상태를 화면/계약 구현으로 기록한다.
  • PList의 기존 한국어 DC_TITLE/LIST_TEXT 2,213행은 read-only 무손상 대조를 마쳤고 통합 selectionMapped=1,603/차단 610이다. 이는 scene/asset/PREPARE/PGM 완료가 아니며 manual named 운영 write→reload는 미검증이다.
  • ThemeA/EList의 expected identity 계약은 구현됐지만 실제 운영 DB에서 KRX/NXT·동명이름·코드 경계와 정확한 selected-row replace/delete를 확인하지 않았다.
  • 원본 종목비교.dat 8쌍은 signed package에서 one-time 실행·재시작 영속성을 확인했다. FSell/VI는 격리 import fixture와 현재 운영 파일 read-only 불변 검증을 마쳤지만 기존 운영 파일은 덮어쓰지 않았다.
  • GraphE·PList/AList·ThemeA·EList의 실제 운영 DB-W와 승인된 5001/5074 밖의 PGM은 미검증이다. 이 작업은 승인된 테스트 데이터·rollback과 장면별 별도 회차가 있어야 한다.

절대 안전 규칙

  1. 앱의 기본값은 항상 DryRun으로 유지한다.
  2. DryRun, --probe, --dry-run, --test-planKTAPConnect를 호출하지 않는다. 이때 Tornado2 Network Monitoring에 기록이 없는 것이 정상이다.
  3. 실제 출력은 승인된 테스트 scene, 지정된 PGM, 지정된 회차에서만 허용한다.
  4. Connect/PREPARE를 시작하기 전에 회차 범위 승인이 있어야 한다. 현재 PGM에 TAKE IN을 보내기 직전에는 같은 회차의 별도 명시적 승인을 다시 받아야 한다.
  5. 승인에 없는 NEXT, Page NEXT, TAKE OUT 이외 명령 또는 다른 scene으로 범위를 넓히지 않는다.
  6. timeout, OutcomeUnknown, WEB_TIMEOUT, refresh fault, 응답/화면 불일치, 대상 프로세스 변경, callback 누락 상태에서는 같은 명령을 자동 또는 수동으로 반복하지 않는다.
  7. 실제 검증 회차에는 reconnectEnabled=false, maximumReconnectAttempts=0을 사용한다. 재접속은 새 회차와 새 승인으로만 수행한다.
  8. 안전 게이트, x64 vendor hash pin, scene allowlist, process/window/port ownership 검사를 완화하지 않는다.
  9. Web 입력으로 object 이름, 파일 경로, K3D method, 임의 SQL을 받지 않는다.
  10. 실제 on-air일 수 있는 scene을 수동으로 unload하거나 COM RCW를 강제 release하지 않는다.
  11. pending Play callback이 있으면 TAKE OUT 이외의 PREPARE/TAKE IN/NEXT/timer refresh를 실행하지 않는다. lifecycle callback이 남아 있으면 Disconnect하지 않는다.

Git 밖에서 준비할 항목

다음 항목은 로컬 운영 설정 또는 승인된 외부 자산이다. Git, MSIX, 로그 첨부, 테스트 fixture에 복사하지 않는다.

  • 원본 test Cuts root: C:\Users\MD\source\repos\MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts
  • 실제 .t2s, image, texture, video와 기타 scene 자산
  • s5006의 상대 자산 Video\큐브배경.vrvs6001 해외지수의 국가별 영상 13개; 현재 위 Cuts root에는 없으므로 제공 전 해당 장면의 실제 PREPARE 금지
  • 정상 앱의 FSell/VI private 저장소 %LOCALAPPDATA%\MBN_STOCK_WEBVIEW\OperatorData. 원본 Data를 strict CP949·hash·durable intent로 검증해 이 위치로 이전하는 one-time importer는 구현했지만, 현재 운영 profile의 기존 파일을 덮어쓰거나 marker를 임의 생성하지 않음
  • Oracle/MariaDB host, SID/service, database, user, password와 운영 query selector
  • K3D/Tornado vendor DLL, Interop, license와 설치 경로
  • MBN_STOCK_K3D_NATIVE_SHA256, MBN_STOCK_K3D_INTEROP_SHA256 승인 값과 승인 근거
  • MSIX 서명 인증서, 개인 키, 암호와 배포용 secrets
  • 실제 Tornado host/port, output channel, PGM 창 정보와 운영 설정
  • Network Monitoring/PGM screenshot·영상·manifest 등 실제 방송 증거

Cuts root는 이번 작업에서 읽기 전용으로 취급한다. 현재 작업 트리의 Test-LegacyCutCoverage.ps1 보강은 기존 45개 active alias 결과와 LegacyCutRequirements.json의 builder 소유 image·texture·video 23개를 함께 검사하는 단계다. 절대 경로, root 탈출, 누락·빈 파일과 root부터 파일까지의 reparse point를 실패로 보고하도록 구성했지만 최종 전체 회귀·패키지 반영 전에는 적용 중으로 판정한다. 현재 승인 Cuts에서는 s6001의 서로 다른 국가 영상 13개와 s5006Video\큐브배경.vrvMissing으로 명시되어야 정상이며, 누락 상태에서는 해당 장면의 PREPARE로 진행하지 않는다.

승인 게이트

Gate A: 회차 범위 승인

Connect 또는 실제 PREPARE 전에 다음 내용을 운영 기록에 남긴다.

[MBN_STOCK_WEBVIEW Tornado 검증 회차]
회차 ID:
예정 시각/최대 종료 시각:
대상 Tornado2/PGM 식별값:
승인된 test cut alias:
승인된 selector와 시작 page:
허용 동작과 횟수: CONNECT, PREPARE, TAKE IN, NEXT/Page NEXT, timer refresh 관찰, TAKE OUT, DISCONNECT
Network Monitoring/PGM 관찰 담당자:
비상 TAKE OUT 담당자:

대상, cut, selector, page, 횟수 중 하나라도 바뀌면 같은 회차 승인을 재사용하지 않는다.

5001 PREPARE 전용 one-shot Gate A

현재 5001/page 1 연결 검증은 New-LegacyPgmPreparePlan.ps1, Invoke-LegacyPgmPrepareGate.ps1, Test-LegacyPgmPrepareGate.mjs의 고정 계약을 사용한다. 계획 생성과 ValidatePlan은 명령을 보내지 않으며, 실행은 clean/pushed main, 정확한 Release x64 MSIX와 package runtime, 원본 5001.t2s, K3D 등록, PGM PID/시작 시각/listener, 15분 이내의 별도 승인 파일이 모두 일치할 때만 열린다. 실제 DTO를 만드는 로컬 database.local.json도 내용이나 비밀번호를 노출하지 않고 경로·크기·SHA-256으로 고정해 실행 동안 read lease를 유지한다.

이 게이트의 허용 예산은 현재 PGM CONNECT 1회와 삼성전자 5001 page 1 PREPARE 1회뿐이다. TAKE IN, NEXT, Page NEXT, TAKE OUT, DISCONNECT, 자동 재연결, DB write와 재시도는 모두 0회다. 앱은 WebView 생성 전에 process-lifetime 권한을 캡처하고, native engine은 동일 권한으로 exact PGM, exact cut lease, exact Live profile과 SDK dispatch 직전 만료를 다시 검사한다. Gate A 동안 DB mutation, 로컬 영구 저장, Fade와 배경 변경도 native 경계 전에 거부한다. Oracle/MariaDB 설정 override와 managed runtime startup hook/profiler 환경값은 process/user/machine 어느 범위에서도 허용하지 않으며 app과 helper의 상속 환경에서도 제거한다.

raw one-shot capability는 명령줄·계획·증적·Web 상태에 기록하지 않는다. package-context wrapper와 Node helper는 current-user ACL named pipe로 각 parent가 OS PID·실행 파일·시작 시각·package/parent identity를 확인한 직후에만 필요한 값을 한 번 받는다. 감시 실패나 제한 시간 초과에서는 pipe를 먼저 폐기하고 control-plane helper만 fail-stop한다. 이는 앱, PGM, Tornado 또는 K3D 프로세스를 종료하거나 vendor cleanup 명령을 보내는 권한이 아니다.

성공 시 앱과 session은 PREPARED 5001 상태로 그대로 두고 별도 승인 없는 cleanup을 하지 않는다. 결과가 불명확하면 helper 증적을 OutcomeUnknown으로 남기고 앱 종료, 반대 명령, 재실행 또는 같은 PREPARE 반복을 하지 않는다. 다음 단계는 Network Monitoring과 PGM을 사람이 확인한 뒤 새 계획과 새 승인을 만드는 것이다.

Gate B: TAKE IN 직전 승인

PREPARE 결과와 Network Monitoring 상태를 운영자가 확인한 후, TAKE IN 직전에 다음과 같이 명시적인 승인을 받아야 한다.

회차 <ID>의 준비된 cut <alias>, page <N>을 현재 PGM에 TAKE IN 1회 실행하는 것을 승인한다.

이 문구가 없거나 회차 ID/cut/page가 다르면 TAKE IN을 실행하지 않는다. 이전 대화의 일반적 동의, 과거 회차 승인, DryRun 성공은 Gate B를 충족하지 않는다.

실제 연결 전 offline preflight

아래 단계는 Tornado에 명령을 보내지 않는다.

  1. 원본 기준선과 cut alias를 확인한다.

    powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-LegacySceneBaseline.ps1
    powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-LegacyCutCoverage.ps1 `
      -CutRoot "C:\Users\MD\source\repos\MBN_STOCK_N\MBN_STOCK_N\bin\Debug\Cuts"
    powershell -NoProfile -ExecutionPolicy Bypass -File .\tests\Scripts\Test-LegacyCutCoverage.Fixture.ps1
    
  2. Debug/Release x64 테스트와 Web bridge 검사를 실행한다.

    dotnet test .\tests\MBN_STOCK_WEBVIEW.Core.Tests\MBN_STOCK_WEBVIEW.Core.Tests.csproj -c Debug -p:Platform=x64
    dotnet test .\tests\MBN_STOCK_WEBVIEW.Core.Tests\MBN_STOCK_WEBVIEW.Core.Tests.csproj -c Release -p:Platform=x64
    dotnet test .\tests\MBN_STOCK_WEBVIEW.Playout.Tests\MBN_STOCK_WEBVIEW.Playout.Tests.csproj -c Debug -p:Platform=x64
    dotnet test .\tests\MBN_STOCK_WEBVIEW.Playout.Tests\MBN_STOCK_WEBVIEW.Playout.Tests.csproj -c Release -p:Platform=x64
    dotnet test .\tests\MBN_STOCK_WEBVIEW.Infrastructure.Tests\MBN_STOCK_WEBVIEW.Infrastructure.Tests.csproj -c Debug -p:Platform=x64
    dotnet test .\tests\MBN_STOCK_WEBVIEW.Infrastructure.Tests\MBN_STOCK_WEBVIEW.Infrastructure.Tests.csproj -c Release -p:Platform=x64
    powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\Test-WebPlayout.ps1
    
  3. 실제 DB smoke는 read-only query만 사용하는 별도 단계로 실행한다. DB 오류가 있으면 Tornado 단계로 넘어가지 않는다.

    dotnet run --project .\tools\MBN_STOCK_WEBVIEW.DbSmoke\MBN_STOCK_WEBVIEW.DbSmoke.csproj -c Release --no-restore
    
  4. 설치된 trusted Release x64 MSIX가 package context에서 실행되는지 확인한다. bin 또는 loose EXE를 실제 검증에 사용하지 않는다.

  5. K3D Registry64, AMD64 PE, TypeLib/Interop metadata, 두 SHA-256 pin, license를 확인한다. DLL을 repo 또는 앱 폴더로 복사해 우회하지 않는다.

  6. 정확히 하나의 승인 대상 Tornado2 프로세스, PGM 창, Network Server TCP port와 LISTEN 소유권을 확인한다. 매뉴얼 예시 port를 추정해 사용하지 않는다.

  7. s5025를 선택했다면 trusted 외부 디렉터리와 파일 preflight가 성공해야 한다. s5006Video\큐브배경.vrv, s6001 해외지수는 선택 국가에 해당하는 Video\20201008_<국가>.vrv가 승인 asset root 안에 있어야 한다. coverage 결과의 MissingAssets, RootEscapes, ReparseAssets 중 하나라도 0이 아니면 해당 장면으로 진행하지 않는다.

  8. 실제 COM을 사용하는 Test/Live playlist의 모든 cut이 로컬 폐쇄형 allowlist 안에 있고 selector가 closed enum/lookup 규칙을 통과하는지 DryRun에서 확인한다. allowlist가 비어 있으면 실제 모드 초기화 자체를 거부해야 한다.

  9. 로컬 playout 설정의 legacySceneFadeDuration 기본값 6과 legacySceneBackgroundKind를 확인한다. 공통 background를 쓸 때 legacySceneBackgroundAssetPathsceneDirectory 아래의 승인된 상대 경로여야 한다. PlayoutSceneCompositionFactory의 DryRun preflight가 파일 존재, 허용 확장자, root 탈출, 절대 경로와 reparse point를 모두 거부하는지 확인한다. Web에는 이 asset 경로를 보내지 않는다.

  10. Web DryRun에서 45개 active alias, row별 enabled, PREPARE 뒤 snapshot freeze, current entry/builder/page size/current rows/last-page/preview와 refresh 상태를 확인한다. pending command와 OutcomeUnknown/timeout quarantine에서도 playlist 편집이 잠겨야 한다. preview에 image/texture/video 경로가 나타나면 실제 회차를 중단한다.

하나라도 실패하면 회차를 시작하지 않는다. 설정을 수정한 뒤 처음부터 새 preflight 결과를 만든다.

정상 실행 순서와 관찰점

1. Connect

  • Gate A와 모든 preflight를 다시 확인한다.
  • Connect는 한 번만 보낸다.
  • OnHello와 Network Monitoring의 request/response를 함께 확인한다.
  • Connect 결과가 accepted이지만 OnHello 또는 monitor 왕복이 확인되지 않으면 준비 완료로 간주하지 않는다.

2. PREPARE

PREPARE는 다음 native 순서를 수행해야 한다.

  1. 현재 활성 playlist 항목과 page 0을 native loader가 조회한다.
  2. scene load, trusted common background와 기본 fade 6 또는 승인된 로컬 fade/scene effect를 적용한다.
  3. BeginTransaction → allowlisted mutation → QueryVariablesEndTransaction을 실행한다.
  4. layout 10을 Prepare한다.

관찰자는 Network Monitoring의 load/prepare 관련 왕복이 한 번씩인지, 앱이 Prepared와 정확한 cue/page를 표시하는지 확인한다. PGM에 의도하지 않은 on-air 변화가 있으면 즉시 회차를 중단하고 장애 절차를 따른다.

3. TAKE IN

  • Gate B를 받은 뒤 한 번만 실행한다.
  • PREPARE 때의 DTO를 재사용하지 않고 frozen entry/page를 실제 DB에서 새로 조회하는지 확인한다.
  • fresh scene의 LoadScene → transaction → Prepare(10)이 명확히 성공한 뒤 Play(10)이 한 번 실행되는지와 OnScenePlayed를 확인한다.
  • PGM에서 원본과 비교할 object 이름별 값, 표시 상태, 색, 위치/크기, crop, path, image/texture/video, fade를 기록한다.
  • Network Monitoring의 두 번째 load/prepare와 PLAY request/response, PGM 화면 시각을 같은 증거 묶음에 보존한다.

4. NEXT

앱이 반환한 nextKind가 기준이다. Web에서 임의로 page/playlist 유형을 정하지 않는다.

  • PageNext: playlist index를 유지하고 원본 Next_Scene(0)처럼 다음 page의 fresh 데이터를 조회한 뒤 새 scene을 LoadScene → transaction mutation → QueryVariablesEndTransactionPrepare(10)Play(10)한다. 이 경로에서 GetPlayingScene in-place 갱신을 기대하지 않는다.
  • PlaylistNext: 현재 page가 마지막일 때 다음 활성 playlist 항목을 load/prepare/play한다. 비활성 항목은 앞으로 건너뛰고 끝에서 wrap하지 않는다.
  • 각 NEXT는 Gate A에 승인된 유형과 횟수 안에서만 한 번씩 실행한다.
  • pageIndex, pageCount, itemCount, 마지막 부분 page의 빈 object clear/hide와 PGM 화면을 함께 확인한다.
  • 모든 operator command는 timer를 먼저 멈춘다. TAKE IN/playlist NEXT 뒤에는 원본 m_time으로 첫 timer가 시작되지만 Page NEXT 성공 뒤에는 timer가 정지 상태로 남아야 한다.

5. Timer refresh

  • 승인 범위에 포함된 경우에만 자동 refresh를 관찰한다. 현재 안전 스케줄러는 직전 OnScenePlayed callback이 lifecycle queue에서 drain된 뒤에만 one-shot cooldown을 시작한다. 5001의 첫 cooldown은 온전한 2초이고, 성공한 refresh 뒤에는 callback drain 후 온전한 3초 recurring cooldown이다. 따라서 실제 dispatch 간격은 callback/drain 시간과 cooldown의 합이며, 원본의 절대 3초 cadence보다 의도적으로 길다.
  • 실제 검증 회차는 trusted JSON에 maximumAutomaticRefreshesPerTakeIn을 승인 횟수와 동일하게 고정한다. Web과 환경 변수는 이 값을 변경할 수 없으며, 상한 도달 후 마지막 callback까지 drain되면 loop가 fault 없이 CAPPED로 끝난다.
  • 이 callback-aware cooldown은 recurring refresh를 제거한 것이 아니다. missed tick을 누적하거나 따라잡지 않고 합치며, refresh는 최대 하나만 in-flight다. 각 실행은 current entry/page의 fresh DB DTO를 사용해 retained on-air scene에 BeginTransaction → mutation → QueryVariablesEndTransactionPrepare(10)Play(10)을 한 번 수행한다.
  • timer refresh는 scene-level background와 fade effect를 다시 적용하지 않는다.
  • 앱의 refreshActive, refreshNextAt, refreshLastSuccessAt, refreshCompletedCount, refreshMaximumCount, refreshLimitReached, fault code/message와 PGM 데이터를 함께 기록한다.
  • refresh가 실패하거나 timeout/unknown이면 fault latch가 켜지고 반복이 즉시 끝나야 한다. TAKE OUT 이외의 명령을 시도하지 않는다.
  • 명확히 성공한 TAKE OUT 뒤 native refresh state가 reset되면 전용 refresh fault marker만 사라져야 한다. OutcomeUnknown, WEB_TIMEOUT 또는 native quarantine 표시는 함께 reset되면 안 된다.
  • operator NEXT는 먼저 현재 refresh epoch를 원자적으로 stop한다. 그 시점에 Play callback이 pending이면 DB activity 대기나 scene query 전에 PLAY_CALLBACK_PENDING으로 즉시 fail-fast하고 자동 재시도하지 않는다. 명령 gate가 바로 풀리므로 TAKE OUT은 계속 사용할 수 있다. callback이 완료된 cooldown 구간에서는 NEXT를 정상 실행할 수 있다.
  • refresh epoch의 cancellation과 status는 같은 lock에서 교체·중지·갱신·완료한다. Stop 뒤 늦은 onDelay, 이전 세대의 success/fault/finally가 현재 세대나 stopped 상태를 다시 활성화할 수 없다.

6. TAKE OUT과 Disconnect

  • 승인된 TAKE OUT 한 번으로 TakeOut(All)/StopAll을 실행한다.
  • OnCutOut 또는 OnStopAll, 앱 state clear와 PGM의 검은 화면 또는 승인된 종료 상태를 확인한다.
  • pending callback과 retired scene 정리가 끝난 뒤에만 Disconnect한다.
  • Network Monitoring에서 stop 계열 왕복과 BYE/disconnect를 확인한다.

Network Monitoring과 PGM 증거 체크리스트

vendor monitor의 실제 명령 표기는 버전에 따라 다를 수 있으므로 추정 문자열로 성공을 만들지 않는다. raw 화면과 시각을 보존하고 다음 의미 단위로 대조한다.

단계 Network Monitoring PGM/앱
Connect HELLO 또는 대응 connect request/response 한 쌍 앱 Connected, 대상 process generation 일치
PREPARE scene load, mutation transaction, prepare의 중복 없는 왕복 Prepared cue/page/row 수 일치, 의도하지 않은 on-air 없음
TAKE IN fresh DB 결과의 두 번째 load/prepare 뒤 PLAY request/response OnScenePlayed, 실제 object 값과 시각 상태 일치
Page NEXT 같은 alias의 새 scene load/transaction/prepare/play 왕복 playlist index 유지, page만 +1, 빈 slot clear/hide, refresh timer 정지
Playlist NEXT 다음 cut load/prepare/play 왕복 다음 활성 항목과 page 0 표시
Timer refresh retained on-air scene의 update transaction, prepare, PLAY 1회의 중복 없는 왕복 callback drain 뒤 첫 full m_time, 이후 callback drain 뒤 full 3초 cooldown, 같은 entry/page의 fresh 값, refresh 상태 정상
TAKE OUT STOP/STOPALL 대응 왕복 OnCutOut/OnStopAll, 승인된 종료 화면
Disconnect BYE 또는 대응 disconnect 왕복 callback pending 0, 연결 해제

회차 증거에는 다음을 포함한다.

  • 회차 ID, package version, Git commit, 실행 시각과 timezone
  • cut alias, closed selector, page index/count, DB 조회 식별값; password와 connection string 제외
  • 각 앱 명령의 결과 code와 OutcomeUnknown 여부
  • 같은 시각의 Network Monitoring과 PGM 화면
  • PREPARE/TAKE IN/NEXT/timer refresh/TAKE OUT 전후 screenshot 또는 연속 capture
  • callback 순서, retired/unloaded scene 수, disconnect 결과
  • Web의 frozen entry/builder/page size/current rows/last-page/preview와 refresh status; asset path는 제외
  • 운영자 최종 판정과 불일치 목록

증거는 Git 제외 로컬 경로 또는 승인된 증거 저장소에 보관한다. 캡처에 host, port, 계정, license 정보가 보이면 외부 전달 전에 별도 보안 절차로 처리하며 원본 증거를 repo에 넣지 않는다.

장애 판정과 반복 금지

상황 즉시 조치 재시도 조건
DB/selector/asset preflight 실패, COM 호출 전 명시적 Rejected 회차 중단, DryRun으로 복귀, 원인 기록 offline 수정과 전체 preflight 후 새 회차 승인
Connect timeout 또는 OnHello/monitor 불일치 결과를 불명확으로 격리, Connect 반복 금지 운영자가 세션과 process/port 상태를 확인한 뒤 새 회차 승인
PREPARE/TAKE IN/NEXT/refresh/TAKE OUT timeout OutcomeUnknown으로 취급, 같은 명령과 반대 명령 자동 실행 금지 PGM/monitor/콜백을 사람이 확인하고 출력 안전을 복구한 뒤 새 회차 승인
WEB_TIMEOUT strict timeout-quarantine message를 native에 전달. MainWindow가 process-lifetime latch를 먼저 세우고 vendor session을 quarantine; 늦은 응답, UI 재시도와 WebView reload로 해제 금지 native status와 PGM을 사람이 대조하고 process를 새로 시작한 뒤 새 승인
refresh DB/scene/COM fault refresh loop 중단, fault latch 유지, TAKE OUT 외 명령 금지 정상 TAKE OUT과 실제 화면 확인 후 offline 원인 수정, 새 회차 승인
명령 성공 응답과 PGM 화면 불일치 앱 state를 신뢰하지 말고 회차 중단 PGM 운영자가 실제 상태를 판정하고 새 회차 승인
callback 누락/지연 scene을 unload하거나 disconnect 강제하지 않음 callback 도착 또는 운영자의 수동 안전 판정 후 새 회차
Tornado process generation, 창, LISTEN 소유권 변경 모든 자동 동작 중지, 대상 격리 새 process를 처음부터 검증하고 새 승인
연결 장애/Disconnect timeout 자동 reconnect 금지, 연결·출력 상태를 불명확으로 기록 운영자가 Tornado 세션을 확인한 뒤 새 회차

OutcomeUnknown은 단순 실패 code가 아니라 실제 출력 결과를 모른다는 latch다. Cancelled, Rejected, WEB_TIMEOUT 또는 UI 오류로 낮추지 않는다. timeout quarantine은 native process-lifetime latch이므로 WebView reload나 오류 창으로 해제되지 않는다. refresh fault marker는 성공한 TAKE OUT 뒤 native reset에만 맞춰 제거할 수 있지만 unknown/quarantine latch에는 영향을 주지 않는다. process를 재시작해 표시를 지우는 행위도 실제 출력 복구가 아니다.

GraphE, ThemeA/EList, FSell/VIList 또는 이름 있는 플레이리스트 쓰기에서 commit·파일 교체 결과나 browser 응답 상관관계를 확인하지 못한 경우에도 동일한 원칙을 적용한다. 어느 한 subsystem에서 OutcomeUnknown이 발생하면 네이티브 global operator-mutation quarantine이 모든 operator write를 프로세스 수명 동안 차단한다. 다른 편집 화면으로 옮겨 쓰거나 같은 값을 다시 저장하지 않는다. 앱을 종료한 뒤 Oracle INPUT_*/SB_*/EXPERT_*/DC_LIST/PLAY_LIST 또는 trusted local file을 읽기 전용으로 대조하고, 실제 결과를 확정·복구한 다음 새 앱 프로세스와 새 명시적 작업으로 시작한다. 송출 engine/workflow가 없거나 IDLE을 양쪽에서 증명하지 못하는 상태, PREPARED/PROGRAM, callback pending, refresh, shutdown 중에도 모든 operator mutation은 fail closed다.

Rollback과 unload

명확히 출력 전 실패한 경우

COM 명령 전 validation에서 명시적으로 거부됐고 Network Monitoring/PGM에 변화가 없음을 확인한 경우에만 offline 수정으로 돌아간다. 수정 후에는 기존 회차를 이어가지 않고 새 회차로 시작한다.

출력이 명확히 active이고 엔진이 정상인 경우

해당 회차에서 명시적으로 승인된 TAKE OUT을 한 번 실행한다. 현재 M 계열에서는 이 권한이 Gate B에만 있다. 성공 callback과 PGM 종료 화면을 확인한 뒤 승인 범위의 Disconnect를 실행하며, 같은 TAKE OUT을 확인용으로 반복하지 않는다.

결과가 불명확한 경우

  1. 앱에서 추가 PREPARE, TAKE IN, NEXT, timer refresh, TAKE OUT, Disconnect를 자동으로 보내지 않는다.
  2. 현재 앱 status, 마지막 명령, Network Monitoring, PGM을 캡처한다.
  3. 방송 운영자가 Tornado 본 프로그램/PGM에서 실제 출력과 세션을 판정한다.
  4. 필요한 수동 정리는 방송 운영 권한과 현장 절차로 수행한다. Codex나 앱이 임의 명령을 추정하지 않는다.
  5. 안전한 black/approved fallback과 세션 종료가 사람에게 확인된 뒤 앱을 종료한다.
  6. 장애 원인과 조치가 확정될 때까지 새 검증을 승인하지 않는다.

Scene 수명 규칙

  • OnScenePlayed가 새 scene의 재생을 확정하면 이전 scene만 retired queue로 이동한다.
  • OnCutOut은 해당 layout, OnStopAll은 전체 player의 on-air 참조를 정리할 근거다.
  • pending Play callback이 있으면 TAKE OUT 이외의 명령을 fail-closed 차단한다. pending OnScenePlayed/OnCutOut/OnStopAll이 있으면 Disconnect와 unload를 시도하지 않는다.
  • CutOut/StopAll pending counter는 SDK dispatch 직전에 증가하고 동기 호출 실패 시 감소한다. 성공 callback은 대응 counter를 하나 감소시키고 중단된 Play의 pending accounting을 취소한다. dispatch 뒤 cancellation/timeout이면 counter 상태를 성공으로 추정하지 않는다.
  • 이전 connection generation에서 늦게 온 callback은 현재 generation의 state나 scene을 정리하는 근거로 쓰지 않는다.
  • 장기 실행 중 retired scene은 callback으로 안전성이 확인된 뒤 STA queue 안에서 Unload와 release를 수행한다.
  • 강제 GC, 임의 RCW release, 현재 on-air scene unload를 rollback으로 사용하지 않는다.

완료 판정

실제 검증 회차는 다음을 모두 만족할 때만 성공이다.

  1. Gate A와 Gate B가 같은 회차에 기록돼 있다.
  2. 허용된 test cut과 selector만 사용했다.
  3. PREPARE → fresh DB TAKE IN → 승인된 callback-aware timer refresh → Page NEXT/playlist NEXT → TAKE OUT 순서가 중복 없이 완료됐다. refresh는 callback drain 뒤 full initial/recurring cooldown과 회차 예산을 지켰다.
  4. 모든 결과가 명확한 성공이며 OutcomeUnknown=false다.
  5. Network Monitoring request/response와 PGM 화면이 같은 시각 증거로 남아 있다.
  6. 원본과 object 값, 표시, 색, 위치/크기, crop, path, asset, fade, page와 refresh 상태가 일치한다. Web preview에는 실제 asset path가 없다.
  7. callback과 unload 순서가 정상이고 Disconnect가 성공했다.
  8. DB password, vendor DLL/license, 인증서/private key, 실제 cut/asset, 운영 설정이 Git 변경에 포함되지 않았다.

검증 후 결과를 SCENE_EQUIVALENCE.md의 해당 행과 LEGACY_FEATURE_AUDIT.md의 관련 항목에 기록한다. 일부 scene 성공이나 과거 runner 성공으로 나머지 행을 일괄 완료 처리하지 않는다.