From 39346ab7184d2f6e115d37a1ed07a4b2c75cdf82 Mon Sep 17 00:00:00 2001 From: to3796zw Date: Tue, 4 Aug 2026 15:45:45 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A0=88=EC=8B=9C=ED=94=BC=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EB=B9=88=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=A0=80=EC=9E=A5=20=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 기동 중 RecipeFieldScan 싱글톤이 비어 있는 상태에서 SaveAndLoadFieldScanFromLasaDb 가 Save 를 Load 보다 먼저 수행해(MainPage.cs:539 -> :540) 대상 FieldHelper.json 이 "[]" 2바이트로 파괴됐다. 현장 3건 확인 — RMS(07-29), U_Frame _pre welding(08-03), Bottom_Frame_Welding(08-03). 3건 모두 Aurora 초기화 직후 4~19초, 사용자 입력이 불가능한 구간이며 형제 TeachingEditor.json 은 옛 mtime 을 유지했다. RecipeFieldScan - SaveFieldHelper 진입부: 완주한 로드의 결과가 아닌 빈 상태면 저장을 건너뛴다. Load(:540)는 그대로 돌아 파일의 실제 데이터로 싱글톤을 채운다. 사용자가 페이지를 지워 정당하게 비운 경우는 loadedOnce 로 구분해 통과시킨다. - LoadFieldHelper: loadedOnce 를 Clear 와 짝지어 리셋하고, 완주해도 결과가 0건이면 로드로 보지 않는다. Clear 이후 예외가 나는 경로에서 플래그만 남아 가드가 무력화되는 것을 막는다. - 맨 catch 를 LastSaveError 기록으로 교체. throw 하지 않는 이유는 ReloadSetting 경로가 Rx OnNext 안이라(UCScannerObjectTeachingViewModel.cs:55-57) 전파 시 구독이 끊겨 티칭 화면 갱신이 영구 정지하기 때문이다. RecipeHelper - SaveRecipe 에 저장 대상과 메모리 신원(Recipe.SelectedRecipeFile) 불일치를 Aurora Logger 로 남기는 관측 코드 추가. 저장은 막지 않는다. 기준을 Dao.SystemRecipe.WorkRecipeFile 로 잡지 않은 것은 그 값이 SystemParameterILSOS.json 에 영속되고 Aurora SystemParameter 와 파일을 공유해 신원으로 쓸 수 없기 때문이다. 교차 오염(내용이 채워진 채 엉뚱한 레시피에 기록되는 현상)은 이 변경으로 막히지 않는다. 관측 로그로 발생 경로를 확정한 뒤 별건으로 조치한다. 검증: LASAGUI/WeldingCommon/ILSOS_PrismaticRD_PTOP 빌드 오류 0, 빌드 산출물 직접 로드한 시뮬레이션 31건 전수 통과. --- .../LASA/GUI/DB/RecipeFieldScan.cs | 42 ++++++++++++++++++- .../WeldingCommon/Utils/RecipeHelper.cs | 32 +++++++++++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/WeldingComponents/LASA/GUI/DB/RecipeFieldScan.cs b/WeldingComponents/LASA/GUI/DB/RecipeFieldScan.cs index 56c26af..ff98b84 100644 --- a/WeldingComponents/LASA/GUI/DB/RecipeFieldScan.cs +++ b/WeldingComponents/LASA/GUI/DB/RecipeFieldScan.cs @@ -65,6 +65,9 @@ namespace DB if (json != null && json != "") jsonData = System.Text.Json.JsonSerializer.Deserialize>(json); else ; + // [추가 2026-08-04] Clear 와 짝을 맞춘다. 이 아래에서 예외가 나면(:77 f.PageName, + // :84 js.PageName 의 NRE 등) 리스트는 빈 채 남으므로 플래그도 false 여야 한다. + loadedOnce = false; fieldScanDatas.Clear(); // default 값 써주기 @@ -84,6 +87,11 @@ namespace DB else AddFieldScanData(js.PageName, defaultFieldScan.FileName, defaultFieldScan.FieldScans); } } + + // [추가 2026-08-04] 완주했더라도 결과가 0건이면(pageCount==0 등) 로드된 것으로 보지 않는다. + // ReloadSetting(MainPage.cs:448/452)/UpdateScoutField(:502/:505)가 Save 를 연속 2회 하므로, + // 여기서 true 로 굳히면 두 번째 Save 가 가드를 통과해 "[]" 를 기록한다. + loadedOnce = fieldScanDatas.Count > 0; } catch (Exception ex) { throw ex; } @@ -95,6 +103,19 @@ namespace DB /// public void SaveFieldHelper(string fileName, string defaultFileStr = null) { + // [추가 2026-08-04] 완주한 로드의 결과가 아닌 빈 상태를 파일에 확정하지 않는다. + // fieldScanDatas 는 프로세스 시작 시 빈 리스트인데 + // SaveAndLoadFieldScanFromLasaDb 는 Save 를 먼저 하므로(MainPage.cs:539 -> :540) + // 기동 중 첫 호출이 대상 파일을 "[]" 로 밀어버린다. 현장 3건 발생 확인. + // Save 만 건너뛰고 :540 Load 는 그대로 돌아 파일의 실제 데이터로 싱글톤을 채운다. + // loadedOnce 로 게이트하는 이유: 사용자가 페이지를 지워 정당하게 비운 경우 + // (DeleteFieldScanData / 호출부 MainPage.cs:904-905)는 저장돼야 하기 때문이다. + if (!loadedOnce && (fieldScanDatas == null || fieldScanDatas.Count == 0)) { + SkippedEmptySaveCount++; + SkippedFiles.Add(fileName ?? "(null)"); + return; + } + try { using (MemoryStream stream = new MemoryStream()) { XmlTextWriter wr = new XmlTextWriter(stream, Encoding.UTF8); @@ -111,9 +132,15 @@ namespace DB var json = System.Text.Json.JsonSerializer.Serialize(fieldScanDatas, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(fileName, json); + LastSaveError = null; // [추가 2026-08-04] 성공했으면 이전 오류를 남기지 않는다 } - } catch { - + } catch (Exception ex) { + // [변경 2026-08-04] 쓰기 실패가 로그 없이 사라져 데이터 소실을 관측할 수 없었다. + // 단 throw 하지 않는다 - ReloadSetting(MainPage.cs:448/452) 경로가 + // Rx OnNext 안이라(UCScannerObjectTeachingViewModel.cs:55-57 Subscribe) + // 전파되면 구독이 끊기고 티칭 화면 갱신이 영구 정지한다. + // MainPage.cs:1433 콤보박스 핸들러에는 try/catch 가 아예 없다. + LastSaveError = $"{fileName} :: {ex.Message}"; } } public List GetPageFieldScan(string pageName) @@ -172,6 +199,17 @@ namespace DB #region 데이터를 위한 Class private List fieldScanDatas = new List(); + + // [추가 2026-08-04] 빈 상태 저장 차단 및 관측용. + // 이 프로젝트(LASAGUI)에는 Aurora 참조가 없어 여기서 로그를 남길 수 없으므로 + // 상태만 노출하고, WeldingCommon 쪽(RecipeHelper)에서 Aurora Logger 로 보고한다. + // loadedOnce 의 의미: "지금 fieldScanDatas 가 완주한 로드의 결과인가". + // 한 번이라도 성공했는가(write-once)가 아니다 - Clear 이후 예외가 나면 다시 false 가 된다. + private bool loadedOnce = false; + public static int SkippedEmptySaveCount = 0; + public static readonly HashSet SkippedFiles = + new HashSet(StringComparer.OrdinalIgnoreCase); + public static string LastSaveError = null; public class FieldScanData { public string PageName { get; set; } diff --git a/WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs b/WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs index 223e018..dc4c84a 100644 --- a/WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs +++ b/WeldingComponents/WeldingCommon/Utils/RecipeHelper.cs @@ -3,6 +3,7 @@ using Aurora.Api; using Aurora.Api.Manager; using DB; using DB.Recipe; +using System; using System.Collections.Generic; using System.Configuration; using System.IO; @@ -16,6 +17,7 @@ namespace WeldingCommon.Utils public static class RecipeHelper { public static (int id, string name) SelectedRecipe; + static Logger Log = new Logger("RecipeHelper"); // [추가 2026-08-04] static IRecipeManager mRecipeManager = Application.Current.AuroraEnv().RecipeManager; static string GetTeachingEditorFullPath(string recipeName) @@ -35,7 +37,35 @@ namespace WeldingCommon.Utils public static void SaveRecipe(string recipeName, bool includeModel = true) { - CommonHelper.GetMainPage().SaveRecipe(GetTeachingEditorFullPath(recipeName), includeModel); + var targetPath = GetTeachingEditorFullPath(recipeName); + + // [추가 2026-08-04] 관측 전용. 저장을 막지 않는다. + // 기준은 DB.Recipe.Recipe.SelectedRecipeFile - Recipe.Load(string) 성공 시에만 + // Recipe.cs:69 에서 세팅되며 디스크를 거치지 않는다. + // (Dao.SystemRecipe.WorkRecipeFile 은 SystemParameterILSOS.json 에 영속되고 + // Aurora SystemParameter 와 파일을 공유하므로 신원 기준으로 쓸 수 없다) + // 두 조건은 독립 신호이므로 else if 로 묶지 않는다. + var loaded = DB.Recipe.Recipe.SelectedRecipeFile; + if (string.IsNullOrWhiteSpace(loaded)) { + Log.W($"[관측] 메모리 신원 없음(로드 이력 없음). target=[{targetPath}]"); + } + if (!string.IsNullOrWhiteSpace(loaded) && + !string.Equals(loaded, targetPath, StringComparison.OrdinalIgnoreCase)) { + Log.W($"[관측] 메모리/대상 불일치. loaded=[{loaded}] target=[{targetPath}]"); + } + + CommonHelper.GetMainPage().SaveRecipe(targetPath, includeModel); + + // 아래는 이번 저장과 무관한 다른 경로(ReloadSetting/UpdateScoutField/콤보박스)에서 + // 누적된 것일 수 있다. 경로는 각 항목의 파일명으로 판독한다. + if (DB.RecipeFieldScan.SkippedEmptySaveCount > 0 || DB.RecipeFieldScan.LastSaveError != null) { + Log.W($"[관측] FieldHelper 저장 생략 {DB.RecipeFieldScan.SkippedEmptySaveCount}회 " + + $"(대상 {DB.RecipeFieldScan.SkippedFiles.Count}종: {string.Join(" | ", DB.RecipeFieldScan.SkippedFiles)}), " + + $"최근오류=[{DB.RecipeFieldScan.LastSaveError}] ※직전 저장과 무관할 수 있음"); + DB.RecipeFieldScan.SkippedEmptySaveCount = 0; + DB.RecipeFieldScan.SkippedFiles.Clear(); + DB.RecipeFieldScan.LastSaveError = null; + } } public static void OnSaveRecipeForEditing()