리크 테스트 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.
 
 
 
 
 

499 lines
23 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)}," +
$"{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 = "")
{
var results = new List<InspectData>();
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<InspectData> ParseLogsFromCsv(DateTime start, DateTime end, string judgmentFilter, string serialFilter, string opFilter, string lotFilter, string modelFilter)
{
var results = new List<InspectData>();
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<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)
{
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<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;
}
}
}
}