using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text; using leak_test_project.Models; namespace leak_test_project.Utils { /// /// 통신 로그 및 시스템 이력을 파일로 저장하는 유틸리티 /// public static class FileLogger { private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); private static readonly object _lock = new object(); /// /// 검사 데이터를 Logs/yyyy-MM-dd.csv 파일에 저장함 (16컬럼) /// public static void LogInspectData(InspectData data) { lock (_lock) { try { if (!Directory.Exists(LogDirectory)) Directory.CreateDirectory(LogDirectory); string dateStr = DateTime.Now.ToString("yyyy-MM-dd"); string filePath = Path.Combine(LogDirectory, $"{dateStr}.csv"); bool isNewFile = !File.Exists(filePath); // 신규 CSV 헤더: DB 스키마 16개 컬럼과 일치 if (isNewFile) { string header = "IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,Lot_No,Jig_No,Channel,Spec_UL,Spec_LL,Leak_Value,Result" + Environment.NewLine; File.WriteAllText(filePath, header, Encoding.UTF8); } string prodDateStr = data.ProductionDate?.ToString("yyyy-MM-dd HH:mm:ss") ?? $"{data.InspectDate} {data.InspectTime}".Trim(); if (string.IsNullOrEmpty(prodDateStr)) { prodDateStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); } string csvLine = $"{Esc(data.IcSn)},{Esc(data.PcbBarcode)},{Esc(data.Maker)},{Esc(data.Model)}," + $"{Esc(data.Variant1)},{Esc(data.Variant2)},{Esc(data.Operator)},{Esc(prodDateStr)}," + $"{Esc(data.Line)},{Esc(data.LotNo)},{Esc(data.JigNo)},{Esc(data.Channel)}," + $"{Esc(data.SpecUL)},{Esc(data.SpecLL)},{Esc(data.MeasuredValue)},{Esc(data.Judgment)}{Environment.NewLine}"; File.AppendAllText(filePath, csvLine, Encoding.UTF8); } catch (Exception ex) { Console.WriteLine($"[FileLogger Error] {ex.Message}"); } } } /// /// 단순 텍스트 로그 (기존 호환성 유지용) /// public static void Log(string tag, string message) { lock (_lock) { try { if (!Directory.Exists(LogDirectory)) Directory.CreateDirectory(LogDirectory); string dateStr = DateTime.Now.ToString("yyyy-MM-dd"); string filePath = Path.Combine(LogDirectory, $"{dateStr}_system.log"); string logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] [{tag}] {message}{Environment.NewLine}"; File.AppendAllText(filePath, logEntry, Encoding.UTF8); } catch (Exception ex) { Console.WriteLine($"[FileLogger Error] {ex.Message}"); } } } /// /// CSV 값 이스케이프: 쉼표, 따옴표, 줄바꿈이 포함된 값을 안전하게 감싸줌 /// private static string Esc(string value) { if (string.IsNullOrEmpty(value)) return ""; if (value.Contains(",") || value.Contains("\"") || value.Contains("\n")) return $"\"{value.Replace("\"", "\"\"")}\""; return value; } } public static class LogParser { private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"); public static List ParseLogs(DateTime start, DateTime end, string judgmentFilter = "[전체]", string serialFilter = "", string opFilter = "", string lotFilter = "", string modelFilter = "", System.Threading.CancellationToken cancellationToken = default) { // 데이터베이스 접속 시도 자체를 수행하지 않고, 오직 로컬 CSV 로그 백업 파일을 분석하여 반환합니다. return ParseLogsFromCsv(start, end, judgmentFilter, serialFilter, opFilter, lotFilter, modelFilter, cancellationToken); } private static List ParseLogsFromCsv(DateTime start, DateTime end, string judgmentFilter, string serialFilter, string opFilter, string lotFilter, string modelFilter, System.Threading.CancellationToken cancellationToken = default) { var results = new List(); if (!Directory.Exists(LogDirectory)) { return results; } // [최적화] 폴더 전체 스캔 대신 조회 기간 내 날짜로 직접 파일 탐색 var files = new List(); for (var dt = start.Date; dt <= end.Date; dt = dt.AddDays(1)) { cancellationToken.ThrowIfCancellationRequested(); string filePath = Path.Combine(LogDirectory, $"{dt:yyyy-MM-dd}.csv"); if (File.Exists(filePath)) { files.Add(filePath); } } foreach (var file in files) { cancellationToken.ThrowIfCancellationRequested(); try { // 엑셀 등으로 파일이 열려 파일 락(Lock)이 걸려있어도 안전하게 읽기 위해 FileShare.ReadWrite 적용 var linesList = new List(); using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) using (var reader = new StreamReader(fs, Encoding.UTF8)) { string lineVal; while ((lineVal = reader.ReadLine()) != null) { cancellationToken.ThrowIfCancellationRequested(); // 라인 읽는 도중 취소 감지 linesList.Add(lineVal); } } var lines = linesList.ToArray(); if (lines.Length <= 1) continue; for (int i = 1; i < lines.Length; i++) { cancellationToken.ThrowIfCancellationRequested(); // 파싱 도중 취소 감지 string line = lines[i]; if (string.IsNullOrWhiteSpace(line)) continue; var parts = line.Split(','); if (parts.Length < 8) continue; InspectData data = null; if (parts.Length >= 16) { // 16개 컬럼 신규 포맷 // IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,Lot_No,Jig_No,Channel,Spec_UL,Spec_LL,Leak_Value,Result string icSn = parts[0].Trim(); string pcbBarcode = parts[1].Trim(); string maker = parts[2].Trim(); string model = parts[3].Trim(); string variant1 = parts[4].Trim(); string variant2 = parts[5].Trim(); string op = parts[6].Trim(); string prodDateRaw = parts[7].Trim(); string lineVal = parts[8].Trim(); string lotNo = parts[9].Trim(); string jigNo = parts[10].Trim(); string channel = parts[11].Trim(); string specUl = parts[12].Trim(); string specLl = parts[13].Trim(); string val = parts[14].Trim(); string judg = parts[15].Trim(); DateTime? prodDate = null; if (DateTime.TryParseExact(prodDateRaw, "yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime dt)) { prodDate = dt; } else if (DateTime.TryParse(prodDateRaw, out DateTime dtFallback)) { prodDate = dtFallback; } // 필터링 적용 (부분 일치 LIKE 검색 흉내) if (judgmentFilter != "[전체]" && judg != judgmentFilter) continue; if (!string.IsNullOrEmpty(serialFilter) && !icSn.Contains(serialFilter) && !pcbBarcode.Contains(serialFilter)) continue; if (!string.IsNullOrEmpty(opFilter) && !op.Contains(opFilter)) continue; if (!string.IsNullOrEmpty(lotFilter) && !lotNo.Contains(lotFilter)) continue; if (!string.IsNullOrEmpty(modelFilter) && !model.Contains(modelFilter)) continue; data = new InspectData { IcSn = icSn, PcbBarcode = pcbBarcode, Maker = maker, Model = model, Variant1 = variant1, Variant2 = variant2, Operator = op, ProductionDate = prodDate, Line = lineVal, LotNo = lotNo, JigNo = jigNo, Channel = channel, SpecUL = specUl, SpecLL = specLl, MeasuredValue = val, Judgment = judg, Retest = "N", // 기존 UI 및 구형 바인딩 호환성용 매핑 ProductId = icSn, InspectDate = prodDate?.ToString("yyyy-MM-dd") ?? "", InspectTime = prodDate?.ToString("HH:mm:ss") ?? "" }; } else { // 8개 컬럼 구형 포맷 // Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment string date = parts[0].Trim(); string time = parts[1].Trim(); string id = parts[2].Trim(); string channel = parts[3].Trim(); string specUl = parts[4].Trim(); string specLl = parts[5].Trim(); string val = parts[6].Trim(); string judg = parts[7].Trim(); // 필터링 적용 (구형 포맷에는 Operator, LotNo, Model이 없으므로 필터값이 있는 경우 결과에서 제외) if (judgmentFilter != "[전체]" && judg != judgmentFilter) continue; if (!string.IsNullOrEmpty(serialFilter) && !id.Contains(serialFilter)) continue; if (!string.IsNullOrEmpty(opFilter)) continue; if (!string.IsNullOrEmpty(lotFilter)) continue; if (!string.IsNullOrEmpty(modelFilter)) continue; DateTime? parsedProdDate = null; string dateTimeStr = $"{date} {time}"; if (DateTime.TryParseExact(dateTimeStr, new[] { "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime dtOld)) { parsedProdDate = dtOld; } else if (DateTime.TryParse(dateTimeStr, out DateTime dtOldFallback)) { parsedProdDate = dtOldFallback; } data = new InspectData { InspectDate = date, InspectTime = time, Channel = channel, ProductId = id, MeasuredValue = val, Judgment = judg, SpecUL = specUl, SpecLL = specLl, Retest = "N", // 정합성을 위해 호환 필드 매핑 IcSn = id, PcbBarcode = id, ProductionDate = parsedProdDate }; } if (data != null) { results.Add(data); } } } catch (Exception ex) { Console.WriteLine($"[LogParser CSV Fallback] Error reading file {file}: {ex.Message}"); } } var sorted = results.OrderByDescending(r => r.InspectDate).ThenByDescending(r => r.InspectTime).ToList(); var uniqueResults = new List(); var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item in sorted) { string id = string.IsNullOrEmpty(item.IcSn) ? item.ProductId : item.IcSn; if (string.IsNullOrEmpty(id) || id == "-") { // 바코드가 없거나 식별 불가한 데이터는 중복 필터링 없이 그대로 출력 uniqueResults.Add(item); } else { // 이미 정렬되어 내림차순이므로 최초로 추가된 건(가장 최신 검사)만 유지 if (seenIds.Add(id)) { uniqueResults.Add(item); } } } return uniqueResults; } } /// /// 데이터를 CSV 파일로 내보내는 유틸리티 /// public static class CsvExporter { /// /// 컬렉션 데이터를 CSV 파일로 저장함 /// /// 데이터 모델 클래스 /// 내보낼 데이터 목록 /// 저장할 파일 경로 public static bool ExportToCsv(IEnumerable items, string filePath) { try { var sb = new StringBuilder(); var props = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance); // Header foreach (var prop in props) { sb.Append(prop.Name).Append(","); } sb.AppendLine(); // Body foreach (var item in items) { foreach (var prop in props) { var val = prop.GetValue(item, null); var str = val?.ToString() ?? ""; if (str.Contains(",") || str.Contains("\"") || str.Contains("\n")) sb.Append($"\"{str.Replace("\"", "\"\"")}\""); else sb.Append(str); sb.Append(","); } sb.AppendLine(); } File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); return true; } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[CsvExporter ExportToCsv Error] {ex.Message}"); return false; } } } }