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.
365 lines
17 KiB
365 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)}," +
|
|
$"{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(',');
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|