// WL-813q3 프로브 — 결과창 「보스 처치 : 미처치 (HP 남음 N %)」 가 813w6 `RunEnded` 페이로드 // `bossHpRatio` 를 쓰는지 실측(에디트 모드 · Play 0 · 로그인 0 · #813). // unity command run_script --file AgentScripts/WL813q3_Probe.cs --entry WL813q3_Probe.RunAll // 산출물: /AgentScripts/WL813q3_PROBE.txt // // 🔴 에셋 저장 0 — WLRunUiSettings 값은 **메모리에서만** 바꾸고 finally 에서 원복한다. // AssetDatabase.SaveAssets / EditorUtility.SetDirty 호출 **0**. 파일 크기·수정시각을 전후로 찍어 증거로 남긴다. // 🔴 Gameplay 는 읽기·공개 API 호출만(파일 수정 0). Play 진입 0. // 🔴 핵심 계측 = **「늦게 그려도 값이 안 바뀐다」**. // RunEnded 를 받은 뒤 BossArena 가 미뤄 둔 Park(만피 복구)을 실제로 돌리고, 보스 HP 바 미러까지 // 100 % 로 만든 다음 결과창을 **다시 그려서** 문구가 그대로 24 % 인지 본다. // 스위치를 끄면(813q2 동작) 같은 자리에서 100 % 가 나온다 = D-44 재현. using System; using System.IO; using System.Text; using UnityEngine; using UnityEditor; using WL.UI; using WL.Combat.Run; using WL.Combat.Boss; public static class WL813q3_Probe { const string kAsset = "Assets/WL/UI/Settings/Resources/WL/WLRunUiSettings.asset"; static StringBuilder _o; static int _pass, _fail, _note; static void H(string s) { _o.AppendLine(); _o.AppendLine("── " + s); } static void N(string s) { _o.AppendLine(" " + s); } static void Note(string s) { _note++; _o.AppendLine(" [NOTE] " + s); } static bool Chk(bool ok, string what) { if (ok) { _pass++; _o.AppendLine(" [PASS] " + what); } else { _fail++; _o.AppendLine(" [FAIL] " + what); } return ok; } static string Root { get { return Directory.GetParent(Application.dataPath).FullName; } } static string OutPath(string n) { return Path.Combine(Path.Combine(Root, "AgentScripts"), n); } /// 본문에서 한 행의 값 문자열을 뽑는다(노드 없이도 되는 유일한 경로 = BodyText). static string BodyRow(string label) { string body = RunResultUI.BodyText; if (string.IsNullOrEmpty(body)) return ""; var lines = body.Split('\n'); for (int i = 0; i < lines.Length; i++) { int k = lines[i].IndexOf(" : ", StringComparison.Ordinal); if (k < 0) continue; if (lines[i].Substring(0, k) == label) return lines[i].Substring(k + 3); } return ""; } /// 합성 RunResult — 배열 3종은 813p 계약대로 길이만 맞춘다. static RunResult Fake(RunOutcome outcome, float bossClearSec, float ratio, double hp, double maxHp) { return new RunResult { outcome = outcome, runIndex = 1, totalSec = 241f, runSeconds = 270f, zoneClearSec = new float[] { 61.5f, -1f }, zoneKills = new int[] { 17, 12 }, zoneCount = 2, zonesCleared = 1, bossGateSec = 170f, bossStartSec = 173f, bossClearSec = bossClearSec, bossHpRatio = ratio, bossHp = hp, bossHpMax = maxHp, kills = 29, bossKills = bossClearSec >= 0f ? 1 : 0, maxChain = 7, maxDamage = 4321d, maxDamageBurst = 8888.5d, levelFrom = 11, levelTo = 12, lootByGrade = new int[] { 0, 5, 2, 0, 0, 0, 0, 0, 0, 0 }, lootTotal = 7, gold = 777, deaths = 1, potionsUsed = 2, skillCasts = 40, time = 0f, frame = 0, }; } public static string RunAll() { _o = new StringBuilder(16000); _o.AppendLine("== WL-813q3 프로브 — 결과창 보스 HP = RunEnded 페이로드 스냅샷 =="); _o.AppendLine(" " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion + " · isPlaying=" + Application.isPlaying + " · project=" + Root); WLRunUiSettings.ClearCache(); var s = WLRunUiSettings.Instance; if (s == null) { _o.AppendLine(" [FAIL] WLRunUiSettings 에셋 없음 — 중단"); return Flush(); } string abs = Path.Combine(Root, kAsset.Replace('/', Path.DirectorySeparatorChar)); var fi0 = new FileInfo(abs); fi0.Refresh(); long len0 = fi0.Length; DateTime mt0 = fi0.LastWriteTimeUtc; // ── 원복용 스냅샷 ──────────────────────────────────────────────────── bool o_payload = s.resultBossHpFromPayload, o_fromUi = s.resultBossHpFromUi; bool o_cards = s.resultCountCardPicksInUi, o_elites = s.resultCountElitesInUi; bool o_enabled = s.enabled_, o_result = s.resultEnabled; var o_rows = s.resultRows; float o_barRatio = BossHpBar.RunBossHpRatio; bool o_subscribed = RunResultUI.Subscribed; try { // ── ⓪ 설정·행 배열 실측 ───────────────────────────────────────── H("⓪ 설정 실측 — 신규 값 2개 · 신규 행 1개"); N("에셋 = " + kAsset + " (" + len0 + " B · " + mt0.ToString("yyyy-MM-dd HH:mm:ss") + " UTC)"); N("resultBossHpFromPayload=" + s.resultBossHpFromPayload + " · resultBossHpFromUi=" + s.resultBossHpFromUi + " · resultCountCardPicksInUi=" + s.resultCountCardPicksInUi + " · resultCountElitesInUi=" + s.resultCountElitesInUi); N("문구 = \"" + s.resultBossNotClearedHpFormat + "\" / \"" + s.resultBossNotClearedText + "\" · 카드 행 이름 = \"" + s.rowCardPicksLabel + "\""); Chk(s.resultBossHpFromPayload, "resultBossHpFromPayload 기본 on(813w6 페이로드 우선)"); Chk(s.resultCountCardPicksInUi, "resultCountCardPicksInUi 기본 on"); Chk(!string.IsNullOrEmpty(s.rowCardPicksLabel), "rowCardPicksLabel 값 있음"); var sbRows = new StringBuilder(); bool hasCard = false, hasElite = false, hasBoss = false; for (int i = 0; i < s.resultRows.Length; i++) { if (i > 0) sbRows.Append(" · "); sbRows.Append(s.resultRows[i]); if (s.resultRows[i] == WLRunResultRow.CardPicks) hasCard = true; if (s.resultRows[i] == WLRunResultRow.EliteKills) hasElite = true; if (s.resultRows[i] == WLRunResultRow.BossClear) hasBoss = true; } N("resultRows(" + s.resultRows.Length + ") = " + sbRows); Chk(hasBoss, "BossClear 행 있음"); Chk(hasElite, "EliteKills 행 있음(813q2)"); Chk(hasCard, "CardPicks 행 있음(813q3 신설)"); // 상자 안에 11행이 들어가는지 산술 실측(제목 + 행 + 버튼) float top = s.resultPadTopPx + s.resultTitleFontPx + s.resultTitleGapPx; float rowsH = s.resultRowHeightPx * s.resultRows.Length; float btnTop = s.resultBoxSizePx.y - (s.resultButtonBottomPx + s.resultButtonSizePx.y); N("배치 = 제목영역 " + top + "px + 행 " + s.resultRows.Length + "×" + s.resultRowHeightPx + "=" + rowsH + "px → 본문 끝 " + (top + rowsH) + "px · 버튼 상단 " + btnTop + "px · 상자 " + s.resultBoxSizePx.y + "px"); Chk(top + rowsH <= btnTop, "행이 버튼을 안 침범(" + (top + rowsH) + " ≤ " + btnTop + ") = 잘림 0(산술)"); // ── ① 813w6 계약 확인 ──────────────────────────────────────────── H("① 813w6 페이로드 계약 — RunResult 에 보스 HP 3필드"); var probe = Fake(RunOutcome.Timeout, -1f, 0.2439f, 439d, 1800d); N("합성 RunResult · bossHpRatio=" + probe.bossHpRatio.ToString("F4") + " bossHp=" + probe.bossHp + " bossHpMax=" + probe.bossHpMax); Chk(Math.Abs(probe.bossHpRatio - 0.2439f) < 1e-5f, "RunResult.bossHpRatio 필드 존재·대입(컴파일이 곧 증거)"); Chk(probe.bossHp == 439d && probe.bossHpMax == 1800d, "RunResult.bossHp / bossHpMax 필드 존재·대입"); N("구독자 수 RunEnded=" + RunEvents.RunEnded.Count + " RunStarted=" + RunEvents.RunStarted.Count); // ── ② 가짜 RunEnded(0.2439) → 「HP 남음 24 %」 ──────────────────── H("② 가짜 RunEnded(bossHpRatio 0.2439) → 「미처치 (HP 남음 24 %)」"); if (!RunResultUI.Subscribed) RunResultUI.Subscribe(true); RunResultUI.ResetState(); BossHpBar.RunBossHpRatio = -1f; // 바는 아무것도 안 물었다 = 폴백이 못 쓰는 상태 RunResultUI.CopyResult(in probe); // = OnRunEnded 가 즉시 하는 그 복사 N("BuildBody → " + RunResultUI.BuildBody(s)); string v2 = BodyRow(s.rowBossClearLabel); N("보스 처치 행 = \"" + v2 + "\" · 원천 = " + RunResultUI.LastBossHpSource + " · 스냅샷 = " + (RunResultUI.BossHpRatioAtEnd * 100f).ToString("F2") + "% (" + RunResultUI.BossHpAtEnd + "/" + RunResultUI.BossHpMaxAtEnd + ")"); Chk(v2 == "미처치 (HP 남음 24 %)", "문구 = \"미처치 (HP 남음 24 %)\" (0.2439 → 24)"); Chk(RunResultUI.LastBossHpSource == "페이로드", "원천 = 페이로드(BossHpBar 를 안 읽었다)"); Chk(Math.Abs(RunResultUI.BossHpRatioAtEnd - 0.2439f) < 1e-5f, "BossHpRatioAtEnd = 0.2439(구조체째 복사)"); Chk(RunResultUI.BossHpAtEnd == 439d && RunResultUI.BossHpMaxAtEnd == 1800d, "BossHp/BossHpMax 스냅샷도 함께 복사"); // ── ③ 🔴 부활 미룸 = 늦게 그려도 스냅샷 유지 ─────────────────────── H("③ 🔴 부활 미룸(늦은 그리기) — Park 로 만피가 되어도 값 불변"); BossArena.SetBossHpForProbe(1f, 1800d, 1800d); // Park → On_Regen 만피 BossHpBar.RunBossHpRatio = 1f; // 813q2 가 읽던 UI 미러도 만피 int deferBefore = BossArena.DeferredParkCount; BossArena.PumpDeferredPark(); // 813w6 이 발행 뒤로 미뤄 둔 고정을 실제로 실행 N("Park 실행 DeferredParkCount " + deferBefore + " → " + BossArena.DeferredParkCount + " · BossHpBar.RunBossHpRatio = " + BossHpBar.RunBossHpRatio.ToString("F2") + " · BossArena.LastBossHpRatio = " + BossArena.LastBossHpRatio.ToString("F2")); string show3 = RunResultUI.Show(); // ← 부활 팝업이 닫힌 뒤의 「늦은 그리기」 N("늦은 Show() → " + show3); string v3 = BodyRow(s.rowBossClearLabel); N("보스 처치 행(늦게) = \"" + v3 + "\" · 원천 = " + RunResultUI.LastBossHpSource); Chk(v3 == "미처치 (HP 남음 24 %)", "🔴 늦게 그려도 24 % 그대로(813w6 §6-1 잔여 해소)"); Chk(Math.Abs(RunResultUI.BossHpRatioAtEnd - 0.2439f) < 1e-5f, "스냅샷 버퍼가 만피에 안 덮인다"); H("③-B C8 — resultBossHpFromPayload = 0 이면 같은 자리에서 100 %(D-44 재현)"); s.resultBossHpFromPayload = false; RunResultUI.BuildBody(s); string v3b = BodyRow(s.rowBossClearLabel); N("행 = \"" + v3b + "\" · 원천 = " + RunResultUI.LastBossHpSource); Chk(v3b == "미처치 (HP 남음 100 %)", "스위치 off → 813q2 동작(만피 100 %) = D-44 재현"); Chk(RunResultUI.LastBossHpSource == "UI바", "off 일 때 원천 = UI바(BossHpBar)"); s.resultBossHpFromPayload = true; RunResultUI.BuildBody(s); Chk(BodyRow(s.rowBossClearLabel) == "미처치 (HP 남음 24 %)", "스위치 복구 → 다시 24 %"); // ── ④ 진짜 RunDirector.EndRun 경로 ───────────────────────────────── H("④ 진짜 발행 경로 — RunDirector.StartRun → EndRun(Timeout)"); BossArena.SetBossHpForProbe(0.2439f, 439d, 1800d); BossHpBar.RunBossHpRatio = -1f; RunResultUI.ResetState(); RunDirector.StartRun("WL813q3 프로브"); int endedBefore = RunResultUI.EndedSeen; // StartRun 이 앞 런을 Abandon 으로 닫을 수 있다 N("StartRun 후 phase=" + RunDirector.Phase + " running=" + RunDirector.IsRunning + " (EndedSeen=" + endedBefore + ")"); if (RunDirector.IsRunning) { RunDirector.EndRun(RunOutcome.Timeout); N("EndRun 후 EndedSeen " + endedBefore + " → " + RunResultUI.EndedSeen + " · LastResult.bossHpRatio=" + RunDirector.LastResult.bossHpRatio.ToString("F4") + " · 결과창 스냅샷=" + RunResultUI.BossHpRatioAtEnd.ToString("F4")); string v4 = BodyRow(s.rowBossClearLabel); N("보스 처치 행(진짜 발행) = \"" + v4 + "\" · 원천 = " + RunResultUI.LastBossHpSource); Chk(RunResultUI.EndedSeen == endedBefore + 1, "결과창이 RunEnded 를 실제로 받았다"); Chk(Math.Abs(RunResultUI.BossHpRatioAtEnd - RunDirector.LastResult.bossHpRatio) < 1e-6f, "결과창 스냅샷 = RunDirector.LastResult.bossHpRatio (같은 값)"); if (RunDirector.LastResult.bossHpRatio >= 0f) { Chk(v4 == "미처치 (HP 남음 24 %)", "진짜 발행에서도 24 %"); // 발행 뒤 Park → 다시 그려도 그대로 BossArena.SetBossHpForProbe(1f, 1800d, 1800d); BossHpBar.RunBossHpRatio = 1f; BossArena.PumpDeferredPark(); RunResultUI.BuildBody(s); Chk(BodyRow(s.rowBossClearLabel) == "미처치 (HP 남음 24 %)", "진짜 발행 + Park 뒤 늦은 그리기도 24 %"); } else { Note("BossArena 가 에디트 모드에서 비활성(snapshotBossHpOnRunEnd/Active) — 페이로드 −1 · " + "값 경로는 ②③ 합성으로만 실측(진짜 발행의 값은 「미확인」)"); } } else { Note("RunDirector 가 에디트 모드에서 안 돈다(WLRunSettings/Cfg) — 진짜 발행 경로는 「미확인」"); } // ── ⑤ 처치 케이스 · 값 없음 폴백 ─────────────────────────────────── H("⑤ 처치 케이스(MM:SS) · 페이로드 −1 폴백 · 둘 다 없음"); RunResultUI.CopyResult(Fake(RunOutcome.Win, 154f, 0f, 0d, 1800d)); RunResultUI.BuildBody(s); string v5a = BodyRow(s.rowBossClearLabel); N("처치 런(bossClearSec 154) = \"" + v5a + "\""); Chk(v5a == "02:34", "처치면 「02:34」(HP 는 안 본다)"); BossHpBar.RunBossHpRatio = 0.436f; // 813q2 가 읽던 값이 살아 있는 상태 RunResultUI.CopyResult(Fake(RunOutcome.Timeout, -1f, -1f, 0d, 0d)); RunResultUI.BuildBody(s); string v5b = BodyRow(s.rowBossClearLabel); N("페이로드 −1 + 바 43.6 % = \"" + v5b + "\" · 원천 = " + RunResultUI.LastBossHpSource); Chk(v5b == "미처치 (HP 남음 44 %)", "페이로드가 −1 이면 813q2 폴백(44 %)"); Chk(RunResultUI.LastBossHpSource == "UI바", "폴백 원천 표시 = UI바"); BossHpBar.RunBossHpRatio = -1f; RunResultUI.BuildBody(s); string v5c = BodyRow(s.rowBossClearLabel); N("둘 다 없음 = \"" + v5c + "\" · 원천 = " + RunResultUI.LastBossHpSource); Chk(v5c == s.resultBossNotClearedText, "둘 다 없으면 퍼센트 없이 「미처치」"); Chk(RunResultUI.LastBossHpSource == "없음", "원천 = 없음"); // ── ⑥ 결과창 행 실측 ─────────────────────────────────────────────── H("⑥ 결과창 행 실측 — 엘리트 · 카드 선택 · 사망/물약 · 본문 전체"); N("813o ZonePickup.PicksThisRun(현재) = " + ZonePickup.PicksThisRun + " · PickCount=" + ZonePickup.PickCount); RunResultUI.CopyResult(Fake(RunOutcome.Timeout, -1f, 0.2439f, 439d, 1800d)); RunResultUI.BuildBody(s); N("카드 선택 종료스냅 = " + RunResultUI.CardPicksAtEnd + " · 엘리트 종료스냅 = " + RunResultUI.EliteKillsAtEnd); N("카드 선택 행 = \"" + BodyRow(s.rowCardPicksLabel) + "\" · 엘리트 행 = \"" + BodyRow(s.rowEliteKillsLabel) + "\" · 사망/물약 행 = \"" + BodyRow(s.rowDeathsLabel) + "\""); Chk(BodyRow(s.rowCardPicksLabel) == RunResultUI.CardPicksAtEnd.ToString(), "카드 선택 행 = 종료 스냅샷 값"); Chk(BodyRow(s.rowEliteKillsLabel) == RunResultUI.EliteKillsAtEnd.ToString(), "엘리트 행 = 종료 스냅샷 값(813q2 그대로)"); Chk(BodyRow(s.rowDeathsLabel) == "1 / 2", "사망/물약 행 = 「1 / 2」(RunResult 값 그대로)"); Chk(RunResultUI.VisibleRows == s.resultRows.Length, "본문 행 수 = resultRows 길이 (" + RunResultUI.VisibleRows + ")"); N("본문 = " + RunResultUI.BodyText.Replace("\n", " | ")); // 카드 선택 스냅샷이 「런 종료 순간」인지 — 뒤에 카운터가 늘어도 화면은 안 바뀐다 N("(카드 카운터는 813o 소유 · 프로브는 읽기만 — 스냅샷 시점만 확인한다)"); // ── ⑦ GC ─────────────────────────────────────────────────────────── H("⑦ GC — 매 프레임 경로 0 B · 이번에 늘어난 줄 0 B"); RunResultUI.Tick(0f); RunResultUI.BuildBody(s); // 워밍업(첫 회 버퍼) long g0 = GC.GetTotalMemory(false); for (int i = 0; i < 100; i++) RunResultUI.Tick(i); long g1 = GC.GetTotalMemory(false); Chk(g1 - g0 == 0, "Tick ×100(매 프레임 경로) Δ = " + (g1 - g0) + " B"); int sink = 0; float fsink = 0f; g0 = GC.GetTotalMemory(false); for (int i = 0; i < 100; i++) { sink += ZonePickup.PicksThisRun; fsink += RunResultUI.BossHpRatioAtEnd; } g1 = GC.GetTotalMemory(false); Chk(g1 - g0 == 0, "이번에 늘어난 읽기(PicksThisRun + BossHpRatioAtEnd) ×100 Δ = " + (g1 - g0) + " B (sink " + sink + "/" + fsink.ToString("F1") + ")"); g0 = GC.GetTotalMemory(false); for (int i = 0; i < 100; i++) RunResultUI.LastBossHpSource = "페이로드"; g1 = GC.GetTotalMemory(false); Chk(g1 - g0 == 0, "LastBossHpSource 대입(리터럴) ×100 Δ = " + (g1 - g0) + " B"); g0 = GC.GetTotalMemory(false); for (int i = 0; i < 100; i++) RunResultUI.BuildBody(s); g1 = GC.GetTotalMemory(false); N("참고 — BuildBody ×100 Δ = " + (g1 - g0) + " B (런 종료 1회 경로 · 813q 부터 본문 문자열을 만든다 · 매 프레임 아님)"); // ── ⑧ C8 롤백 ────────────────────────────────────────────────────── H("⑧ C8 롤백 — 값 하나씩"); s.resultCountCardPicksInUi = false; RunResultUI.BuildBody(s); Chk(BodyRow(s.rowCardPicksLabel) == "0", "resultCountCardPicksInUi=0 → 카드 선택 행 「0」"); s.resultCountCardPicksInUi = true; s.resultRows = new WLRunResultRow[] { WLRunResultRow.TotalTime, WLRunResultRow.BossClear }; RunResultUI.BuildBody(s); Chk(RunResultUI.VisibleRows == 2 && BodyRow(s.rowCardPicksLabel) == "", "resultRows 에서 CardPicks 를 빼면 행이 사라진다"); s.resultRows = o_rows; RunResultUI.BuildBody(s); Chk(RunResultUI.VisibleRows == o_rows.Length, "행 배열 복구 = " + RunResultUI.VisibleRows + "행"); s.resultBossHpFromPayload = false; s.resultBossHpFromUi = false; RunResultUI.BuildBody(s); Chk(BodyRow(s.rowBossClearLabel) == s.resultBossNotClearedText, "둘 다 off → 퍼센트 없이 「미처치」"); s.resultBossHpFromPayload = o_payload; s.resultBossHpFromUi = o_fromUi; WLRunUiSettings.RuntimeDisabled = true; string tickOff = RunResultUI.Tick(0f); N("RuntimeDisabled Tick → " + tickOff); Chk(tickOff.StartsWith("off"), "RuntimeDisabled=1 → Tick 무동작"); WLRunUiSettings.RuntimeDisabled = false; Chk(RunResultUI.Tick(0f) == "tick ok", "복구 → tick ok"); s.resultEnabled = false; Chk(RunResultUI.Tick(0f).StartsWith("off"), "resultEnabled=0 → 무동작"); s.resultEnabled = o_result; // ── ⑨ 뒷정리 확인 ────────────────────────────────────────────────── H("⑨ 뒷정리 — 에셋 저장 0 · 구독·주입 원복"); BossArena.SetBossHpForProbe(-2f, 0d, 0d); // 주입 해제 BossHpBar.RunBossHpRatio = o_barRatio; RunResultUI.ResetState(); if (!o_subscribed) RunResultUI.Subscribe(false); N("BossArena 주입 해제 · BossHpBar.RunBossHpRatio=" + BossHpBar.RunBossHpRatio + " · 결과창 구독=" + RunResultUI.Subscribed + "(시작 시 " + o_subscribed + ")"); N("BossArena.BossHpSnapshots=" + BossArena.BossHpSnapshots + " DeferredParkCount=" + BossArena.DeferredParkCount); } catch (Exception ex) { _fail++; _o.AppendLine(" [FAIL] 예외 — " + ex.GetType().Name + " : " + ex.Message); _o.AppendLine(ex.StackTrace); } finally { // 🔴 SO 원복(메모리만 · 저장 0) s.resultBossHpFromPayload = o_payload; s.resultBossHpFromUi = o_fromUi; s.resultCountCardPicksInUi = o_cards; s.resultCountElitesInUi = o_elites; s.enabled_ = o_enabled; s.resultEnabled = o_result; s.resultRows = o_rows; WLRunUiSettings.RuntimeDisabled = false; try { BossArena.SetBossHpForProbe(-2f, 0d, 0d); } catch { } BossHpBar.RunBossHpRatio = o_barRatio; } var fi1 = new FileInfo(abs); fi1.Refresh(); N("에셋 후 = " + fi1.Length + " B · " + fi1.LastWriteTimeUtc.ToString("yyyy-MM-dd HH:mm:ss") + " UTC"); Chk(fi1.Length == len0 && fi1.LastWriteTimeUtc == mt0, "에셋 파일 무변동 = 저장 0(SaveAssets/SetDirty 호출 0)"); N("SO 값 원복 = payload:" + s.resultBossHpFromPayload + " fromUi:" + s.resultBossHpFromUi + " cards:" + s.resultCountCardPicksInUi + " elites:" + s.resultCountElitesInUi + " enabled_:" + s.enabled_ + " result:" + s.resultEnabled + " 행수:" + s.resultRows.Length); return Flush(); } static string Flush() { _o.AppendLine(); _o.AppendLine("== 합계 PASS " + _pass + " / FAIL " + _fail + " / NOTE " + _note + " =="); string p = OutPath("WL813q3_PROBE.txt"); try { File.WriteAllText(p, _o.ToString(), new UTF8Encoding(true)); } catch { } return _o.ToString() + "\n(파일: " + p + ")"; } }