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 = "") { var results = new List(); string dbIniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Database.ini"); string connectionString = string.Empty; try { connectionString = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString; } catch { // 로드 실패 시 무시 } if (string.IsNullOrWhiteSpace(connectionString)) { // DB 연결 주소가 정의되지 않은 경우 로컬 CSV 탐색으로 Fallback return ParseLogsFromCsv(start, end, judgmentFilter, serialFilter, opFilter, lotFilter, modelFilter); } try { using (var conn = new System.Data.SqlClient.SqlConnection(connectionString)) { conn.Open(); // DATETIME2(0) 밀리초 오차 없이 안전한 인덱스 스캔을 위해 < @EndLimit 비교식 사용 string query = @" SELECT 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 FROM [dbo].[EOL] WHERE Production_Date >= @Start AND Production_Date < @EndLimit"; if (judgmentFilter != "[전체]") { query += " AND Result = @Judgment"; } if (!string.IsNullOrEmpty(serialFilter)) { query += " AND (IC_SN LIKE @Serial OR PCB_Barcode LIKE @Serial)"; } if (!string.IsNullOrEmpty(opFilter)) { query += " AND Operator LIKE @Operator"; } if (!string.IsNullOrEmpty(lotFilter)) { query += " AND Lot_No LIKE @LotNo"; } if (!string.IsNullOrEmpty(modelFilter)) { query += " AND Model LIKE @Model"; } query += " ORDER BY Production_Date DESC"; using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn)) { cmd.Parameters.AddWithValue("@Start", start.Date); cmd.Parameters.AddWithValue("@EndLimit", end.Date.AddDays(1)); // 익일 00:00:00 미만으로 설정하여 당일 23:59:59까지 포함 if (judgmentFilter != "[전체]") { cmd.Parameters.AddWithValue("@Judgment", judgmentFilter); } if (!string.IsNullOrEmpty(serialFilter)) { cmd.Parameters.AddWithValue("@Serial", $"%{serialFilter}%"); } if (!string.IsNullOrEmpty(opFilter)) { cmd.Parameters.AddWithValue("@Operator", $"%{opFilter}%"); } if (!string.IsNullOrEmpty(lotFilter)) { cmd.Parameters.AddWithValue("@LotNo", $"%{lotFilter}%"); } if (!string.IsNullOrEmpty(modelFilter)) { cmd.Parameters.AddWithValue("@Model", $"%{modelFilter}%"); } using (var reader = cmd.ExecuteReader()) { while (reader.Read()) { var icSn = reader.IsDBNull(0) ? "" : reader.GetString(0); var pcbBarcode = reader.IsDBNull(1) ? "" : reader.GetString(1); var maker = reader.IsDBNull(2) ? "" : reader.GetString(2); var model = reader.IsDBNull(3) ? "" : reader.GetString(3); var variant1 = reader.IsDBNull(4) ? "" : reader.GetString(4); var variant2 = reader.IsDBNull(5) ? "" : reader.GetString(5); var op = reader.IsDBNull(6) ? "" : reader.GetString(6); var prodDate = reader.IsDBNull(7) ? (DateTime?)null : reader.GetDateTime(7); var line = reader.IsDBNull(8) ? "" : reader.GetString(8); var lotNo = reader.IsDBNull(9) ? "" : reader.GetString(9); var jigNo = reader.IsDBNull(10) ? "" : reader.GetString(10); var channel = reader.IsDBNull(11) ? "" : reader.GetString(11); var specUl = reader.IsDBNull(12) ? 0m : reader.GetDecimal(12); var specLl = reader.IsDBNull(13) ? 0m : reader.GetDecimal(13); var leakValue = reader.IsDBNull(14) ? 0m : reader.GetDecimal(14); var result = reader.IsDBNull(15) ? "NG" : reader.GetString(15); var data = new InspectData { IcSn = icSn, PcbBarcode = pcbBarcode, Maker = maker, Model = model, Variant1 = variant1, Variant2 = variant2, Operator = op, ProductionDate = prodDate, Line = line, LotNo = lotNo, JigNo = jigNo, Channel = channel, SpecUL = specUl.ToString("F2"), SpecLL = specLl.ToString("F2"), MeasuredValue = leakValue.ToString("F4"), Judgment = result, Retest = "N", // 기존 레거시 UI 바인딩 호환성 유지용 매핑 ProductId = icSn, InspectDate = prodDate?.ToString("yyyy-MM-dd") ?? "", InspectTime = prodDate?.ToString("HH:mm:ss") ?? "" }; results.Add(data); } } } } } catch (Exception ex) { Console.WriteLine($"[DB LogParser Error] {ex.Message}. Falling back to CSV."); // DB 접속이나 쿼리 에러 발생 시 안전하게 CSV 로그 파일 파싱으로 대체 return ParseLogsFromCsv(start, end, judgmentFilter, serialFilter, opFilter, lotFilter, modelFilter); } return results; } private static List ParseLogsFromCsv(DateTime start, DateTime end, string judgmentFilter, string serialFilter, string opFilter, string lotFilter, string modelFilter) { var results = new List(); if (!Directory.Exists(LogDirectory)) { return results; } // 파일명의 yyyy-MM-dd 날짜 파싱 시 시스템 로캘 영향을 막기 위해 TryParseExact 사용 var files = Directory.GetFiles(LogDirectory, "*.csv") .Where(f => { string fileName = Path.GetFileNameWithoutExtension(f); if (DateTime.TryParseExact(fileName, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime fileDate)) { return fileDate.Date >= start.Date && fileDate.Date <= end.Date; } return false; }); foreach (var file in files) { 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) { linesList.Add(lineVal); } } var lines = linesList.ToArray(); if (lines.Length <= 1) continue; for (int i = 1; i < lines.Length; i++) { 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; } } } }