using Microsoft.Office.Interop.Excel; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using System.Xml; using VisionCommon; using static GUI.MainPage; using static System.Windows.Forms.VisualStyles.VisualStyleElement; namespace DB { public class RecipeFieldScan { private static readonly RecipeFieldScan instance = new RecipeFieldScan(); static RecipeFieldScan() { } private RecipeFieldScan() { } public static RecipeFieldScan Instance { get { return instance; } } private string defaultScoutName = "D:\\ILSOSConfig\\ILSOS_PrismaticScanner\\_vision\\FieldScanImage\\Default\\DefaultFieldScanImage.Jpeg"; #region GUI에서 활용하는 함수 (영역 업데이트, 필드 로드, 필드 저장, 필드 영역 전달 등등) public void UpdateScoutField(List fieldScan, int selectedIndex) { if (fieldScanDatas.Count() - 1 < selectedIndex) { for (int i = fieldScanDatas.Count() - 1; i < selectedIndex; i++) { if (fieldScanDatas.Count() == 0) AddFieldScanData(GetAutoPageName(selectedIndex), defaultScoutName, fieldScan); else { string str = GetPageImageName(GetAutoPageName(fieldScanDatas.Count - 1), ref defaultScoutName); AddFieldScanData(GetAutoPageName(selectedIndex), str, fieldScan); } } } else { ChangeFieldScan(GetAutoPageName(selectedIndex), fieldScan); } } public void LoadFieldHelper(string fileName, int pageCount, string defaultFileStr = null, List defaultChildBufRowCols=null) { try { XmlDocument doc = new XmlDocument(); var extenstion = Path.GetExtension(fileName); if (!File.Exists(fileName)) File.WriteAllText(fileName, ""); if (extenstion == ".json") { var jsonString = File.ReadAllText(fileName); } else { FileInfo fi = new FileInfo(fileName); StreamReader sr = new StreamReader(fileName); doc.Load(sr); sr.Dispose(); } XmlElement root = doc.DocumentElement; // JSON 파일 읽기, JSON을 List로 변환 string json = File.ReadAllText(fileName); List jsonData = new List(); 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 값 써주기 FieldScanData defaultFieldScan = new FieldScanData("none", defaultFileStr, defaultChildBufRowCols); // page 개수보다 입력된 개수가 작을 경우(비정상)와 정상인 경우에 따라 다르게 저장. if (jsonData.Count != pageCount) { for (int i = 0; i < pageCount; i++) { // PageName이 동일하다면 그대로 써줄 것. 아니라면, 초기 값을 써줄 것. var modifyFieldScan = jsonData.FirstOrDefault(f => f.PageName.Equals(GetAutoPageName(i), StringComparison.OrdinalIgnoreCase)); if (modifyFieldScan != null && modifyFieldScan.FieldScans != null && modifyFieldScan.FileName != null) AddFieldScanData(modifyFieldScan.PageName, modifyFieldScan.FileName, modifyFieldScan.FieldScans); else AddFieldScanData(GetAutoPageName(i), defaultFieldScan.FileName, defaultFieldScan.FieldScans); } } else { foreach (FieldScanData js in jsonData) { if (js != null && js.FieldScans != null && js.FileName != null) AddFieldScanData(js.PageName, js.FileName, js.FieldScans); 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; } } /// /// Scout 후, Field Size 변경 후, Recipe 저장 시 FieldScan 저장되어야 함. /// /// /// 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); wr.Formatting = System.Xml.Formatting.Indented; wr.Flush(); foreach(FieldScanData fieldScan in fieldScanDatas) { fieldScan.FieldScans .OrderBy(row => row.RowIdx) .ThenBy(col => col.ColIdx) .Select(param => param.Clone() as ChildBufRowCol) .ToList(); } var json = System.Text.Json.JsonSerializer.Serialize(fieldScanDatas, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(fileName, json); LastSaveError = null; // [추가 2026-08-04] 성공했으면 이전 오류를 남기지 않는다 } } 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) { var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase)); if (modifyFieldScan != null) { return modifyFieldScan.FieldScans .OrderBy(row => row.RowIdx) .ThenBy(col => col.ColIdx) .Select(param => param.Clone() as ChildBufRowCol) .ToList(); } else return null; } public string GetAutoPageName(int pageNo) { return string.Format("selectedIndex{0}", pageNo); } #endregion #region Dictionary 관리를 위한 함수 void AddFieldScanData(string pageName, string fileName, List childBuf) { fieldScanDatas.Add(new FieldScanData(pageName,fileName,childBuf)); for (int i = 0; i < fieldScanDatas.Count(); i++) { fieldScanDatas[i].PageName = GetAutoPageName(i); } } public void DeleteFieldScanData(string pageName) { fieldScanDatas = fieldScanDatas.Where(fieldScan => fieldScan.PageName != pageName).ToList(); for (int i = 0; i < fieldScanDatas.Count(); i++) { fieldScanDatas[i].PageName = GetAutoPageName(i); } } bool ChangeFieldScan(string pageName, List childBuf) { var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase)); if (modifyFieldScan != null) { modifyFieldScan.FieldScans = childBuf; return true; } else return false; } public bool ChangeFieldString(string pageName, string fileName) { var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase)); if (modifyFieldScan != null) { modifyFieldScan.FileName = fileName; return true; } else return false; } public string GetPageImageName(string pageName, ref string defaultStr) { var modifyFieldScan = fieldScanDatas.FirstOrDefault(f => f.PageName.Equals(pageName, StringComparison.OrdinalIgnoreCase)); if (modifyFieldScan != null) { return modifyFieldScan.FileName; } else return defaultStr; } #endregion #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; } public string FileName { get; set; } public List FieldScans { get; set; } public FieldScanData(string pageName, string fileName, List fieldScans) { this.PageName = pageName; this.FileName = fileName; this.FieldScans = fieldScans; } } #endregion } }