아모센스 마킹 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.
 
 
 
 
 

495 lines
24 KiB

using System;
using System.Configuration;
using marking_gui.Models;
namespace marking_gui.Services
{
public class DatabaseService
{
private readonly string _connectionString;
private readonly bool _offlineMode = false;
private readonly System.Collections.Concurrent.ConcurrentDictionary<string, int> _offlineLotSequences = new System.Collections.Concurrent.ConcurrentDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
public bool OfflineMode => _offlineMode;
public DatabaseService()
{
try
{
string dbServer = string.Empty;
string dbName = string.Empty;
string dbUser = string.Empty;
string dbPassword = string.Empty;
string rawConnStr = string.Empty;
// AppDomain.CurrentDomain.BaseDirectory 에서 Database.ini 로드 시도
string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Database.ini");
if (System.IO.File.Exists(iniPath))
{
var lines = System.IO.File.ReadAllLines(iniPath);
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith(";") || trimmed.StartsWith("#"))
{
continue;
}
if (trimmed.StartsWith("ConnectionString", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
rawConnStr = parts[1].Trim();
}
}
else if (trimmed.StartsWith("Db_Server", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
dbServer = parts[1].Trim();
}
}
else if (trimmed.StartsWith("Db_Name", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
dbName = parts[1].Trim();
}
}
else if (trimmed.StartsWith("Db_User", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
dbUser = parts[1].Trim();
}
}
else if (trimmed.StartsWith("Db_Password", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
dbPassword = parts[1].Trim();
}
}
else if (trimmed.StartsWith("OfflineMode", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
bool.TryParse(parts[1].Trim(), out _offlineMode);
}
}
}
}
// 개별 설정이 있는 경우 조합해서 사용
if (!string.IsNullOrWhiteSpace(dbServer) &&
!string.IsNullOrWhiteSpace(dbName) &&
!string.IsNullOrWhiteSpace(dbUser) &&
!string.IsNullOrWhiteSpace(dbPassword))
{
string cleanServer = dbServer.Trim().Replace(" ", "");
_connectionString = $"Server={cleanServer};Database={dbName};User Id={dbUser};Password={dbPassword};";
}
else
{
_connectionString = rawConnStr;
}
// Database.ini 에 없을 경우 App.config 백업 사용
if (string.IsNullOrEmpty(_connectionString))
{
var connSettings = ConfigurationManager.ConnectionStrings["MssqlConnection"];
if (connSettings != null)
{
_connectionString = connSettings.ConnectionString;
}
else
{
_connectionString = "Server=localhost;Database=MarkingDb;Trusted_Connection=True;";
}
}
// SSL 인증서 체인 신뢰성 오류(TrustServerCertificate=True 누락) 방지 조치 추가
if (!string.IsNullOrWhiteSpace(_connectionString) && _connectionString.IndexOf("TrustServerCertificate", StringComparison.OrdinalIgnoreCase) < 0)
{
if (!_connectionString.EndsWith(";")) _connectionString += ";";
_connectionString += "TrustServerCertificate=True;";
}
// DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 3초로 조정
if (!string.IsNullOrWhiteSpace(_connectionString) &&
_connectionString.IndexOf("Connect Timeout", StringComparison.OrdinalIgnoreCase) < 0 &&
_connectionString.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0)
{
if (!_connectionString.EndsWith(";")) _connectionString += ";";
_connectionString += "Connect Timeout=3;";
}
}
catch (Exception ex)
{
_connectionString = string.Empty;
System.Diagnostics.Debug.WriteLine("설정 로드 중 예외 발생: " + ex.Message);
}
}
public bool CheckConnection()
{
return CheckConnection(out _);
}
public bool CheckConnection(out string errorMessage)
{
errorMessage = string.Empty;
if (_offlineMode)
return true;
if (string.IsNullOrWhiteSpace(_connectionString))
{
errorMessage = "데이터베이스 연결 문자열이 설정되지 않았습니다. (Database.ini 파일 점검 필요)";
return false;
}
try
{
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{
conn.Open();
return true;
}
}
catch (Exception ex)
{
errorMessage = ex.Message;
System.Diagnostics.Debug.WriteLine("[DB CheckConnection] 연결 실패: " + ex.Message);
return false;
}
}
public Product GetProductBySerial(string serial)
{
if (string.IsNullOrEmpty(serial))
return null;
string searchSerial = serial.Trim().ToUpper();
// 오프라인 모드인 경우 데이터베이스가 없으므로 항상 이전 공정 통과(PASS)로 가정하여 가상 데이터 반환
if (_offlineMode)
{
System.Diagnostics.Debug.WriteLine("[OfflineMode] 제품 조회 우회 (오프라인 모드 활성화). S/N: " + searchSerial);
return new Product(searchSerial, "PASS", "PASS", "PASS");
}
try
{
// 이전 3개 공정 테이블(Housing_Assembly, Cal_EOL_Result, EOL)에서 IC_SN 기준으로 Result 각각 조회
// Housing_Assembly 테이블에서 PCB_Barcode 가져옴
string query = @"
SELECT
(SELECT TOP 1 [Result] FROM [dbo].[Housing_Assembly] WHERE UPPER([IC_SN]) = @Serial) AS HousingResult,
(SELECT TOP 1 [Final_Result] FROM [dbo].[Cal_EOL_Result] WHERE UPPER([IC_SN]) = @Serial) AS CalResult,
(SELECT TOP 1 [Result] FROM [dbo].[EOL] WHERE UPPER([IC_SN]) = @Serial) AS EolResult,
(SELECT TOP 1 [PCB_Barcode] FROM [dbo].[Housing_Assembly] WHERE UPPER([IC_SN]) = @Serial) AS PcbBarcode";
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{
using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
{
cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
cmd.Parameters.AddWithValue("@Serial", searchSerial);
conn.Open();
using (var reader = cmd.ExecuteReader())
{
if (reader.Read())
{
// 데이터베이스 값이 NULL 이거나 빈 값일 경우 "대기"로 처리
string housingRes = reader["HousingResult"] != DBNull.Value ? reader["HousingResult"].ToString().Trim() : "대기";
string calRes = reader["CalResult"] != DBNull.Value ? reader["CalResult"].ToString().Trim() : "대기";
string eolRes = reader["EolResult"] != DBNull.Value ? reader["EolResult"].ToString().Trim() : "대기";
string pcbBarcode = reader["PcbBarcode"] != DBNull.Value ? reader["PcbBarcode"].ToString().Trim() : string.Empty;
// 세 공정 중 하나라도 데이터 흔적이 있는 경우 Product 객체 생성 반환
if (housingRes != "대기" || calRes != "대기" || eolRes != "대기")
{
return new Product(searchSerial, housingRes, calRes, eolRes)
{
PcbBarcode = pcbBarcode
};
}
}
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[DB GetProduct] 이전 공정 결과 조회 중 에러: " + ex.Message);
throw;
}
return null;
}
/// <summary>
/// [dbo].[Marking] 테이블에 해당 IC S/N 데이터가 이미 존재하는지 중복 검사를 수행합니다.
/// </summary>
public bool IsProductExists(string serial)
{
if (_offlineMode || string.IsNullOrWhiteSpace(serial))
{
return false;
}
try
{
string query = "SELECT COUNT(1) FROM [dbo].[Marking] WHERE [IC_SN] = @Serial;";
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{
using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
{
cmd.CommandTimeout = 3;
cmd.Parameters.AddWithValue("@Serial", serial.Trim());
conn.Open();
int count = (int)cmd.ExecuteScalar();
return count > 0;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[DB IsProductExists] 중복 조회 중 예외: " + ex.Message);
return false;
}
}
/// <summary>
/// 최종 성능 검출 값을 데이터베이스에 저장합니다.
/// 세 계측값은 현장 테이블의 DECIMAL(5, 2)에 맞춰 저장합니다 (Overload: 999.00).
/// </summary>
public bool SaveProductResult(
string serial,
string maker,
string model,
string variant1,
string variant2,
string operatorId,
string line,
string lotNo,
string jigNo,
double voltage,
double current,
double resistance,
string result,
DateTime startTime,
DateTime endTime,
string markedSerial,
string pcbBarcode = null,
string drawingNo = "",
string hkmcNo = "")
{
// Overload NG follows the same no-save rule as every other NG, even for direct callers.
if (result != "PASS" && result != "OK")
{
LoggerService.Info($"[DB 저장 생략] S/N: {serial}, 결과: {result} (NG는 저장하지 않음)");
return false;
}
var measurementParameters = CreateMeasurementParameters(voltage, current, resistance);
decimal roundedVoltage = (decimal)measurementParameters[0].Value;
decimal roundedCurrent = (decimal)measurementParameters[1].Value;
decimal roundedResistance = (decimal)measurementParameters[2].Value;
// IC_SN 및 PCB_Barcode 컬럼 규격(NVARCHAR(30), NOT NULL) 방어 처리
if (string.IsNullOrWhiteSpace(serial))
{
serial = "UNKNOWN_" + DateTime.Now.ToString("HHmmss");
}
if (serial.Length > 30)
{
serial = serial.Substring(0, 30);
}
string safeResult = "OK";
// DB 컬럼 길이 한계에 맞춰서 문자열 안전하게 자르기 (Truncate)
string safeMaker = string.IsNullOrEmpty(maker) ? "" : (maker.Length > 10 ? maker.Substring(0, 10) : maker);
string safeModel = string.IsNullOrEmpty(model) ? "" : (model.Length > 10 ? model.Substring(0, 10) : model);
string safeVariant1 = string.IsNullOrEmpty(variant1) ? "" : (variant1.Length > 10 ? variant1.Substring(0, 10) : variant1);
string safeVariant2 = string.IsNullOrEmpty(variant2) ? "" : (variant2.Length > 10 ? variant2.Substring(0, 10) : variant2);
string safeOperator = string.IsNullOrEmpty(operatorId) ? "" : (operatorId.Length > 10 ? operatorId.Substring(0, 10) : operatorId);
string safeLine = string.IsNullOrEmpty(line) ? "" : (line.Length > 10 ? line.Substring(0, 10) : line);
string safeLotNo = string.IsNullOrEmpty(lotNo) ? "" : (lotNo.Length > 10 ? lotNo.Substring(0, 10) : lotNo);
string safeJigNo = string.IsNullOrEmpty(jigNo) ? "" : (jigNo.Length > 10 ? jigNo.Substring(0, 10) : jigNo);
DateTime prodDate = endTime;
// QR Code 조합 포맷 생성 ([IC S/N];[도면품번];[HKMC품번];[제품 S/N];) (Marking_QR: NVARCHAR(60))
string markingQr = string.Format("{0};{1};{2};{3};", serial, drawingNo ?? "", hkmcNo ?? "", markedSerial);
if (markingQr.Length > 60)
{
markingQr = markingQr.Substring(0, 60);
}
// 오프라인 모드인 경우 실제 DB 저장을 생략하고 정상 통과 처리
if (_offlineMode)
{
if (safeResult == "OK" && !string.IsNullOrWhiteSpace(safeLotNo) && !string.IsNullOrWhiteSpace(markedSerial) && markedSerial.Length >= 4)
{
string seqStr = markedSerial.Substring(markedSerial.Length - 4);
if (int.TryParse(seqStr, out int seqNum))
{
_offlineLotSequences.AddOrUpdate(safeLotNo, seqNum, (key, oldVal) => Math.Max(oldVal, seqNum));
}
}
System.Diagnostics.Debug.WriteLine(string.Format(
"[OfflineMode] DB 저장 생략 (오프라인 모드 활성화) S/N: {0} | 각인번호: {1} | QR: {2} | Maker: {3} | Model: {4} | Line: {5} | LotNo: {6} | Vol_1: {7:F2}V | Cur_1: {8:F2}mA | Res_1: {9:F3}kΩ | 결과: {10} | 시작:{11} | 종료:{12}",
serial, markedSerial, markingQr, safeMaker, safeModel, safeLine, safeLotNo, roundedVoltage, roundedCurrent, roundedResistance, result,
startTime.ToString("HH:mm:ss"), endTime.ToString("HH:mm:ss")));
return true;
}
string safePcbBarcode = string.IsNullOrWhiteSpace(pcbBarcode) ? serial : pcbBarcode.Trim();
if (safePcbBarcode.Length > 30)
{
safePcbBarcode = safePcbBarcode.Substring(0, 30);
}
// 실제 MSSQL DB 적재 실행 (동일 IC_SN 재검사 시 데이터가 덮어씌워지도록 MERGE 문 도입)
try
{
string query = @"MERGE INTO [dbo].[Marking] AS target
USING (SELECT @Serial AS IC_SN) AS source
ON (target.[IC_SN] = source.IC_SN)
WHEN MATCHED THEN
UPDATE SET
[PCB_Barcode] = @PcbBarcode,
[Maker] = @Maker,
[Model] = @Model,
[Variant_1] = @Var1,
[Variant_2] = @Var2,
[Operator] = @Operator,
[Production_Date] = @ProdDate,
[Line] = @Line,
[Lot_No] = @LotNo,
[Jig_No] = @JigNo,
[PT_Vol_1] = @Vol1,
[PT_Current_1] = @Cur1,
[PT_Resistance_1] = @Res1,
[Marking_QR] = @Qr,
[Result] = @Result
WHEN NOT MATCHED THEN
INSERT ([IC_SN], [PCB_Barcode], [Marking_QR], [Maker], [Model], [Variant_1], [Variant_2], [Operator], [Production_Date], [Line], [Lot_No], [Jig_No], [PT_Vol_1], [PT_Current_1], [PT_Resistance_1], [Result])
VALUES (@Serial, @PcbBarcode, @Qr, @Maker, @Model, @Var1, @Var2, @Operator, @ProdDate, @Line, @LotNo, @JigNo, @Vol1, @Cur1, @Res1, @Result);";
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{
using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
{
cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
cmd.Parameters.AddWithValue("@Serial", serial);
cmd.Parameters.AddWithValue("@PcbBarcode", safePcbBarcode);
cmd.Parameters.AddWithValue("@Qr", markingQr);
cmd.Parameters.AddWithValue("@Maker", safeMaker);
cmd.Parameters.AddWithValue("@Model", safeModel);
cmd.Parameters.AddWithValue("@Var1", safeVariant1);
cmd.Parameters.AddWithValue("@Var2", safeVariant2);
cmd.Parameters.AddWithValue("@Operator", safeOperator);
cmd.Parameters.AddWithValue("@ProdDate", prodDate);
cmd.Parameters.AddWithValue("@Line", safeLine);
cmd.Parameters.AddWithValue("@LotNo", safeLotNo);
cmd.Parameters.AddWithValue("@JigNo", safeJigNo);
cmd.Parameters.AddRange(measurementParameters);
cmd.Parameters.AddWithValue("@Result", safeResult);
conn.Open();
cmd.ExecuteNonQuery();
}
}
// 성공 로그 출력 및 true 반환
System.Diagnostics.Debug.WriteLine(string.Format(
"[DB 저장 로그] S/N: {0} | 각인번호: {1} | QR: {2} | Maker: {3} | Model: {4} | Line: {5} | LotNo: {6} | Vol_1: {7:F2}V | Cur_1: {8:F2}mA | Res_1: {9:F3}kΩ | 결과: {10} | 시작:{11} | 종료:{12}",
serial, markedSerial, markingQr, safeMaker, safeModel, safeLine, safeLotNo, roundedVoltage, roundedCurrent, roundedResistance, result,
startTime.ToString("HH:mm:ss"), endTime.ToString("HH:mm:ss")));
return true;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[DB SaveResult] DB 저장 실패 (메시지: " + ex.Message + ")");
throw; // 예외 원인을 상위로 전파하여 UI 상태창에 구체적인 오류 원인(ex.Message)이 그대로 노출되도록 함
}
}
internal static System.Data.SqlClient.SqlParameter[] CreateMeasurementParameters(double voltage, double current, double resistance)
{
return new[]
{
new System.Data.SqlClient.SqlParameter("@Vol1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 2, Value = Math.Round((decimal)voltage, 2) },
new System.Data.SqlClient.SqlParameter("@Cur1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 2, Value = Math.Round((decimal)current, 2) },
new System.Data.SqlClient.SqlParameter("@Res1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 2, Value = Math.Round((decimal)resistance, 2) }
};
}
/// <summary>
/// 지정한 LotNo로 기존에 저장된 제품들의 각인 순번 중 최대값을 구하여 다음 각인 일련번호(MAX + 1)를 구합니다.
/// 동일 시료 재검사 시 DB 행이 MERGE UPDATE 되어 레코드 수(COUNT)가 증가하지 않더라도 순번은 지속 증가합니다.
/// </summary>
public int GetNextMarkingSequence(string lotNo)
{
if (string.IsNullOrWhiteSpace(lotNo)) return 1;
if (_offlineMode)
{
_offlineLotSequences.TryGetValue(lotNo.Trim(), out int maxSeq);
return maxSeq + 1;
}
try
{
// SQL Server 2022 호환: Marking_QR에서 맨 뒤 4자리를 TRY_CAST로 안전하게 정수로 변환하여 MAX 순번 산출 (NOLOCK 적용)
string query = @"
SELECT ISNULL(MAX(
TRY_CAST(
RIGHT(
RTRIM(
CASE
WHEN RTRIM([Marking_QR]) LIKE '%;' THEN LEFT(RTRIM([Marking_QR]), LEN(RTRIM([Marking_QR])) - 1)
ELSE RTRIM([Marking_QR])
END
), 4
) AS INT
)
), 0)
FROM [dbo].[Marking] WITH (NOLOCK)
WHERE [Lot_No] = @LotNo
AND [Marking_QR] IS NOT NULL
AND [Marking_QR] NOT LIKE N'NG:%'
AND LEN(RTRIM([Marking_QR])) >= 4;";
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{
using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
{
cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
cmd.Parameters.AddWithValue("@LotNo", lotNo.Trim());
conn.Open();
object resultObj = cmd.ExecuteScalar();
int maxSeq = (resultObj != null && resultObj != DBNull.Value) ? Convert.ToInt32(resultObj) : 0;
return maxSeq + 1;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[DB GetNextMarkingSequence] 에러 발생: " + ex.Message);
throw; // 예외 원인을 상위로 전파하여 UI 상태창에 구체적인 원인 출력
}
}
}
}