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
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|