리크 테스트 gui
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

366 lines
17 KiB

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
{
/// <summary>
/// 통신 로그 및 시스템 이력을 파일로 저장하는 유틸리티
/// </summary>
public static class FileLogger
{
private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
private static readonly object _lock = new object();
/// <summary>
/// 검사 데이터를 Logs/yyyy-MM-dd.csv 파일에 저장함 (16컬럼)
/// </summary>
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)}," +
2 months ago
$"{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}");
}
}
}
/// <summary>
/// 단순 텍스트 로그 (기존 호환성 유지용)
/// </summary>
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}");
}
}
}
/// <summary>
/// CSV 값 이스케이프: 쉼표, 따옴표, 줄바꿈이 포함된 값을 안전하게 감싸줌
/// </summary>
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<InspectData> 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<InspectData> ParseLogsFromCsv(DateTime start, DateTime end, string judgmentFilter, string serialFilter, string opFilter, string lotFilter, string modelFilter, System.Threading.CancellationToken cancellationToken = default)
{
var results = new List<InspectData>();
if (!Directory.Exists(LogDirectory))
{
return results;
}
// [최적화] 폴더 전체 스캔 대신 조회 기간 내 날짜로 직접 파일 탐색
var files = new List<string>();
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<string>();
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(',');
2 months ago
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<InspectData>();
var seenIds = new HashSet<string>(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;
}
}
/// <summary>
/// 데이터를 CSV 파일로 내보내는 유틸리티
/// </summary>
public static class CsvExporter
{
/// <summary>
/// 컬렉션 데이터를 CSV 파일로 저장함
/// </summary>
/// <typeparam name="T">데이터 모델 클래스</typeparam>
/// <param name="items">내보낼 데이터 목록</param>
/// <param name="filePath">저장할 파일 경로</param>
public static bool ExportToCsv<T>(IEnumerable<T> 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;
}
}
}
}