Browse Source

260813 아모센스 납품(PCB 바코드 없이 검사 가능)

main
최승민 3 months ago
parent
commit
92a602072b
  1. 9
      DB/dbo.EOL_Schema.sql
  2. 5
      leak_test_project/App.xaml.cs
  3. 2
      leak_test_project/Models/ProjectModels.cs
  4. 2
      leak_test_project/Properties/PublishProfiles/FolderProfile.pubxml.user
  5. 168
      leak_test_project/Services/BarcodeSettings.cs
  6. 23
      leak_test_project/Services/Board4251.cs
  7. 2
      leak_test_project/Services/EolDatabaseService.cs
  8. 74
      leak_test_project/Services/TestProcessService.cs
  9. 7
      leak_test_project/Setting.ini
  10. 8
      leak_test_project/ViewModels/HomeViewModel.cs
  11. 6
      leak_test_project/Views/ParametersWindow.xaml
  12. 15
      leak_test_project/Views/ParametersWindow.xaml.cs
  13. 3
      leak_test_project/leak_test_project.csproj
  14. 1
      ob/01_System_Architecture.md
  15. 2
      ob/02_DB_Design_ERD.md
  16. 36
      ob/05_Implementation_History.md
  17. 4
      ob/db_login_integration.md

9
DB/dbo.EOL_Schema.sql

@ -30,7 +30,7 @@ GO
-- 3. 개편된 테이블 생성 -- 3. 개편된 테이블 생성
CREATE TABLE [dbo].[EOL] ( CREATE TABLE [dbo].[EOL] (
[IC_SN] NVARCHAR(10) NOT NULL, -- 제품의 최종 조합 ID (ProductId) (기본키) [IC_SN] NVARCHAR(10) NOT NULL, -- 제품의 최종 조합 ID (ProductId) (기본키)
[PCB_Barcode] NVARCHAR(10) NOT NULL, -- 고유 식별용 바코드 정보 (UK) [PCB_Barcode] NVARCHAR(10) NULL, -- 고유 식별용 바코드 정보 (NULL 허용)
[Maker] NVARCHAR(10) NULL, -- 로그인 메이커 정보 [Maker] NVARCHAR(10) NULL, -- 로그인 메이커 정보
[Model] NVARCHAR(10) NULL, -- 로그인 모델 정보 [Model] NVARCHAR(10) NULL, -- 로그인 모델 정보
[Variant_1] NVARCHAR(10) NULL, -- 로그인 변형1 정보 [Variant_1] NVARCHAR(10) NULL, -- 로그인 변형1 정보
@ -46,11 +46,14 @@ CREATE TABLE [dbo].[EOL] (
[Leak_Value] DECIMAL(5,4) NOT NULL, -- 측정된 누설량 [Leak_Value] DECIMAL(5,4) NOT NULL, -- 측정된 누설량
[Result] NVARCHAR(5) NOT NULL, -- 합격 여부 판정 (OK / NG) [Result] NVARCHAR(5) NOT NULL, -- 합격 여부 판정 (OK / NG)
CONSTRAINT [PK_EOL_IC_SN] PRIMARY KEY CLUSTERED ([IC_SN] ASC), CONSTRAINT [PK_EOL_IC_SN] PRIMARY KEY CLUSTERED ([IC_SN] ASC)
CONSTRAINT [UQ_EOL_PCB_Barcode] UNIQUE ([PCB_Barcode] ASC)
); );
GO GO
-- PCB_Barcode가 NULL인 행을 다수 허용하는 Filtered Unique Index
CREATE UNIQUE NONCLUSTERED INDEX [UQ_EOL_PCB_Barcode] ON [dbo].[EOL]([PCB_Barcode] ASC) WHERE [PCB_Barcode] IS NOT NULL;
GO
PRINT 'Table [dbo].[EOL] created successfully with latest 16 columns and IC_SN as Primary Key.'; PRINT 'Table [dbo].[EOL] created successfully with latest 16 columns and IC_SN as Primary Key.';
GO GO

5
leak_test_project/App.xaml.cs

@ -51,13 +51,14 @@ namespace leak_test_project
"OfflinePreview=false\r\n"; "OfflinePreview=false\r\n";
System.IO.File.WriteAllText(dbIniPath, defaultContent, System.Text.Encoding.UTF8); System.IO.File.WriteAllText(dbIniPath, defaultContent, System.Text.Encoding.UTF8);
} }
// Setting.ini 파일 사전 생성 보장
Housing.Services.BarcodeSettings.EnsureDefaultFile(Housing.Services.BarcodeSettings.GetDefaultFilePath());
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine($"[App Database.ini Create Error] {ex.Message}"); System.Diagnostics.Debug.WriteLine($"[App Database.ini Create Error] {ex.Message}");
} }
// Logs 폴더 사전 생성 보장 (실행 폴더 기준)
try try
{ {
// 실행 폴더(C:\LeakTest\) 기준 Logs 폴더 생성 // 실행 폴더(C:\LeakTest\) 기준 Logs 폴더 생성

2
leak_test_project/Models/ProjectModels.cs

@ -102,7 +102,7 @@ namespace leak_test_project.Models
// 신규 DB 테이블 EOL 컬럼 매핑 및 정합성 보장을 위한 추가 프로퍼티 // 신규 DB 테이블 EOL 컬럼 매핑 및 정합성 보장을 위한 추가 프로퍼티
public string IcSn { get; set; } = string.Empty; public string IcSn { get; set; } = string.Empty;
public string PcbBarcode { get; set; } = string.Empty; public string PcbBarcode { get; set; } = null;
public string Maker { get; set; } = string.Empty; public string Maker { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty; public string Model { get; set; } = string.Empty;
public string Variant1 { get; set; } = string.Empty; public string Variant1 { get; set; } = string.Empty;

2
leak_test_project/Properties/PublishProfiles/FolderProfile.pubxml.user

@ -2,7 +2,7 @@
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. --> <!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<History>True|2026-07-24T02:09:35.2227587Z||;True|2026-07-24T11:05:48.8845478+09:00||;True|2026-07-24T11:05:14.3843421+09:00||;True|2026-07-24T11:04:27.7256207+09:00||;True|2026-07-24T11:00:36.3011595+09:00||;True|2026-07-24T10:55:51.6585002+09:00||;True|2026-07-24T10:55:02.4837763+09:00||;True|2026-07-24T10:50:36.6197914+09:00||;True|2026-07-24T10:49:58.5199665+09:00||;True|2026-07-24T10:49:17.2856270+09:00||;True|2026-07-24T10:48:16.6349673+09:00||;True|2026-07-24T10:45:17.3653596+09:00||;True|2026-07-24T10:41:32.2281978+09:00||;True|2026-07-24T10:39:09.8181835+09:00||;True|2026-07-24T10:30:36.6348545+09:00||;True|2026-07-24T10:28:51.9168366+09:00||;True|2026-07-24T10:24:29.6224638+09:00||;True|2026-07-21T10:52:14.1396563+09:00||;True|2026-07-21T10:51:15.9786396+09:00||;True|2026-07-21T10:49:23.8272985+09:00||;True|2026-07-21T10:43:12.2452400+09:00||;True|2026-07-02T11:10:03.3205130+09:00||;True|2026-07-02T10:57:07.7675289+09:00||;True|2026-07-02T10:48:02.4094102+09:00||;True|2026-07-02T10:47:32.3161255+09:00||;True|2026-07-02T09:56:10.5544859+09:00||;True|2026-07-01T16:42:19.4986373+09:00||;True|2026-07-01T16:30:40.3015254+09:00||;True|2026-07-01T16:15:05.1946766+09:00||;True|2026-07-01T16:14:42.7624440+09:00||;True|2026-07-01T16:10:15.7662884+09:00||;True|2026-07-01T11:40:58.7303122+09:00||;True|2026-07-01T10:52:14.8628105+09:00||;True|2026-07-01T10:45:31.8793742+09:00||;True|2026-07-01T10:35:19.6809435+09:00||;True|2026-07-01T10:35:00.9643317+09:00||;True|2026-07-01T10:32:31.0544609+09:00||;True|2026-07-01T10:25:33.3316628+09:00||;True|2026-07-01T10:23:02.2841540+09:00||;True|2026-07-01T10:19:59.2997126+09:00||;False|2026-07-01T10:17:21.1004186+09:00||;False|2026-06-30T10:32:04.0883654+09:00||;True|2026-06-30T10:24:54.7745409+09:00||;True|2026-06-25T14:28:15.0458856+09:00||;True|2026-06-25T13:57:36.3233571+09:00||;True|2026-06-25T12:56:04.8828587+09:00||;</History> <History>True|2026-08-13T04:58:48.9691782Z||;False|2026-08-13T13:58:15.9192707+09:00||;True|2026-08-12T12:41:55.0758627+09:00||;True|2026-08-04T16:36:36.4086339+09:00||;True|2026-07-24T11:09:35.2227587+09:00||;True|2026-07-24T11:05:48.8845478+09:00||;True|2026-07-24T11:05:14.3843421+09:00||;True|2026-07-24T11:04:27.7256207+09:00||;True|2026-07-24T11:00:36.3011595+09:00||;True|2026-07-24T10:55:51.6585002+09:00||;True|2026-07-24T10:55:02.4837763+09:00||;True|2026-07-24T10:50:36.6197914+09:00||;True|2026-07-24T10:49:58.5199665+09:00||;True|2026-07-24T10:49:17.2856270+09:00||;True|2026-07-24T10:48:16.6349673+09:00||;True|2026-07-24T10:45:17.3653596+09:00||;True|2026-07-24T10:41:32.2281978+09:00||;True|2026-07-24T10:39:09.8181835+09:00||;True|2026-07-24T10:30:36.6348545+09:00||;True|2026-07-24T10:28:51.9168366+09:00||;True|2026-07-24T10:24:29.6224638+09:00||;True|2026-07-21T10:52:14.1396563+09:00||;True|2026-07-21T10:51:15.9786396+09:00||;True|2026-07-21T10:49:23.8272985+09:00||;True|2026-07-21T10:43:12.2452400+09:00||;True|2026-07-02T11:10:03.3205130+09:00||;True|2026-07-02T10:57:07.7675289+09:00||;True|2026-07-02T10:48:02.4094102+09:00||;True|2026-07-02T10:47:32.3161255+09:00||;True|2026-07-02T09:56:10.5544859+09:00||;True|2026-07-01T16:42:19.4986373+09:00||;True|2026-07-01T16:30:40.3015254+09:00||;True|2026-07-01T16:15:05.1946766+09:00||;True|2026-07-01T16:14:42.7624440+09:00||;True|2026-07-01T16:10:15.7662884+09:00||;True|2026-07-01T11:40:58.7303122+09:00||;True|2026-07-01T10:52:14.8628105+09:00||;True|2026-07-01T10:45:31.8793742+09:00||;True|2026-07-01T10:35:19.6809435+09:00||;True|2026-07-01T10:35:00.9643317+09:00||;True|2026-07-01T10:32:31.0544609+09:00||;True|2026-07-01T10:25:33.3316628+09:00||;True|2026-07-01T10:23:02.2841540+09:00||;True|2026-07-01T10:19:59.2997126+09:00||;False|2026-07-01T10:17:21.1004186+09:00||;False|2026-06-30T10:32:04.0883654+09:00||;True|2026-06-30T10:24:54.7745409+09:00||;True|2026-06-25T14:28:15.0458856+09:00||;True|2026-06-25T13:57:36.3233571+09:00||;True|2026-06-25T12:56:04.8828587+09:00||;</History>
<LastFailureDetails /> <LastFailureDetails />
</PropertyGroup> </PropertyGroup>
</Project> </Project>

168
leak_test_project/Services/BarcodeSettings.cs

@ -0,0 +1,168 @@
using System;
using System.IO;
namespace Housing.Services
{
/// <summary>
/// Setting.ini (또는 Database.ini) 파일에서 바코드 관련 설정(BarcodeMapping)을 관리하는 클래스.
/// </summary>
public class BarcodeSettings
{
/// <summary>
/// 바코드 매핑 필수 여부.
/// true : 바코드 매핑 실패 시 검사 중단 (NG 처리)
/// false: 바코드 매핑 안 되어도 검사 계속 진행 (더미 데이터 생성 안함)
/// </summary>
public bool BarcodeMapping { get; set; } = true;
private static readonly object _barcodeLock = new object();
private static BarcodeSettings _cachedSettings;
private static DateTime _lastWriteTime;
private static string _cachedPath;
public static string GetDefaultFilePath()
{
string folder = AppDomain.CurrentDomain.BaseDirectory;
if (!Directory.Exists(folder))
{
Directory.CreateDirectory(folder);
}
string settingIni = Path.Combine(folder, "Setting.ini");
if (File.Exists(settingIni)) return settingIni;
string lowerSettingIni = Path.Combine(folder, "setting.ini");
if (File.Exists(lowerSettingIni)) return lowerSettingIni;
return settingIni;
}
public static BarcodeSettings Load(string path = null)
{
if (string.IsNullOrEmpty(path))
{
path = GetDefaultFilePath();
}
lock (_barcodeLock)
{
try
{
if (File.Exists(path))
{
var currentWriteTime = File.GetLastWriteTime(path);
if (_cachedSettings != null && _cachedPath == path && _lastWriteTime == currentWriteTime)
{
return _cachedSettings;
}
var settings = new BarcodeSettings();
var lines = File.ReadAllLines(path);
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith(";") || trimmed.StartsWith("#")) continue;
if (trimmed.StartsWith("BarcodeMapping", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length == 2)
{
if (bool.TryParse(parts[1].Trim(), out var val))
{
settings.BarcodeMapping = val;
}
else if (int.TryParse(parts[1].Trim(), out var intVal))
{
settings.BarcodeMapping = intVal != 0;
}
}
}
}
_cachedSettings = settings;
_lastWriteTime = currentWriteTime;
_cachedPath = path;
return settings;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[BarcodeSettings Load Error] {ex.Message}");
}
// 파일이 없을 경우 Setting.ini 자동 생성
EnsureDefaultFile(path);
return _cachedSettings ?? new BarcodeSettings();
}
}
public static void Save(BarcodeSettings settings, string path = null)
{
if (string.IsNullOrEmpty(path))
{
path = GetDefaultFilePath();
}
lock (_barcodeLock)
{
try
{
string dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
using (var writer = new StreamWriter(path, false, System.Text.Encoding.UTF8))
{
writer.WriteLine("; =====================================================================");
writer.WriteLine("; Leak Test System - 환경 및 바코드 설정 파일 (Setting.ini)");
writer.WriteLine("; =====================================================================");
writer.WriteLine();
writer.WriteLine("[Barcode]");
writer.WriteLine("; BarcodeMapping: 바코드 매핑 필수 여부 (true = 매핑 미될 시 검사 중단, false = 매핑 없어도 검사 진행)");
writer.WriteLine($"BarcodeMapping={(settings.BarcodeMapping ? "true" : "false")}");
}
_cachedSettings = settings;
_cachedPath = path;
_lastWriteTime = File.GetLastWriteTime(path);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[BarcodeSettings Save Error] {ex.Message}");
}
}
}
public static void EnsureDefaultFile(string path)
{
try
{
if (!File.Exists(path))
{
string dir = Path.GetDirectoryName(path);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
using (var writer = new StreamWriter(path, false, System.Text.Encoding.UTF8))
{
writer.WriteLine("; =====================================================================");
writer.WriteLine("; Leak Test System - 환경 및 바코드 설정 파일 (Setting.ini)");
writer.WriteLine("; =====================================================================");
writer.WriteLine();
writer.WriteLine("[Barcode]");
writer.WriteLine("; BarcodeMapping: 바코드 매핑 필수 여부 (true = 매핑 미될 시 검사 중단, false = 매핑 없어도 검사 진행)");
writer.WriteLine("BarcodeMapping=true");
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[BarcodeSettings EnsureDefaultFile Error] {ex.Message}");
}
}
}
}

23
leak_test_project/Services/Board4251.cs

@ -153,12 +153,15 @@ namespace leak_test_project.Services
foreach (var line in lines) foreach (var line in lines)
{ {
string trimmed = line.Trim(); string trimmed = line.Trim();
if (trimmed.Contains("x00c_") || trimmed.IndexOf("<end>", StringComparison.OrdinalIgnoreCase) >= 0 || trimmed.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) if (trimmed.Contains("x00c_") || trimmed.IndexOf("<end>", StringComparison.OrdinalIgnoreCase) >= 0 || trimmed.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0 || trimmed.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0)
continue; continue;
// 16자리 영숫자 ID 추출 // 시리얼 ID 추출 (우선순위: 16자리 정확 매칭 -> 8~32자리 가변 길이 매칭)
var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); var match16 = System.Text.RegularExpressions.Regex.Match(trimmed, @"\b[A-Za-z0-9]{16}\b");
if (match.Success) return match.Value; if (match16.Success) return match16.Value;
var matchGen = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9\-_]{8,32}");
if (matchGen.Success) return matchGen.Value;
} }
return null; return null;
@ -255,14 +258,14 @@ namespace leak_test_project.Services
} }
else else
{ {
// <end>가 오지 않았더라도, 명령어 에코가 아닌 줄에서 16자리 ID를 발견하면 즉시 완료 처리 // <end>가 오지 않았더라도, 명령어 에코가 아닌 독립된 데이터 줄에서 ID를 발견하면 완료 처리
string[] lines = currentContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); string[] lines = currentContent.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var line in lines) foreach (var line in lines)
{ {
string trimmed = line.Trim(); string trimmed = line.Trim();
if (trimmed.Contains("x00c_")) continue; if (trimmed.Contains("x00c_") || trimmed.IndexOf("x00o", StringComparison.OrdinalIgnoreCase) >= 0) continue;
var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9]{16}"); var match = System.Text.RegularExpressions.Regex.Match(trimmed, @"[A-Za-z0-9\-_]{8,32}");
if (match.Success) if (match.Success)
{ {
isComplete = true; isComplete = true;
@ -327,9 +330,9 @@ namespace leak_test_project.Services
// 0. 초기화 (x00o 전송) // 0. 초기화 (x00o 전송)
ProgressMessage?.Invoke(this, "4251 보드 초기화 중..."); ProgressMessage?.Invoke(this, "4251 보드 초기화 중...");
// x00o 명령을 보내고 응답 내용에 상관없이 다음 단계 진행 // x00o 명령을 비동기 완료 시까지 대기(await)하여 락(SemaphoreSlim) 블로킹을 방지함
_ = _service.SendCommandAsync("x00o\r\n"); await _service.SendCommandAsync("x00o\r\n");
await Task.Delay(350); // 보드 안정화 대기 await Task.Delay(200); // 보드 안정화 대기
// 1. 보드 상태 확인 (Fail인 경우 중단) // 1. 보드 상태 확인 (Fail인 경우 중단)
ProgressMessage?.Invoke(this, "4251 보드 데이터 읽는 중..."); ProgressMessage?.Invoke(this, "4251 보드 데이터 읽는 중...");

2
leak_test_project/Services/EolDatabaseService.cs

@ -71,7 +71,7 @@ namespace leak_test_project.Services
using (var cmd = new SqlCommand(query, conn)) using (var cmd = new SqlCommand(query, conn))
{ {
cmd.Parameters.AddWithValue("@IC_SN", string.IsNullOrEmpty(inspectData.IcSn) ? "-" : inspectData.IcSn); cmd.Parameters.AddWithValue("@IC_SN", string.IsNullOrEmpty(inspectData.IcSn) ? "-" : inspectData.IcSn);
cmd.Parameters.AddWithValue("@PCB_Barcode", string.IsNullOrEmpty(inspectData.PcbBarcode) ? "-" : inspectData.PcbBarcode); cmd.Parameters.AddWithValue("@PCB_Barcode", string.IsNullOrEmpty(inspectData.PcbBarcode) || inspectData.PcbBarcode == "-" ? (object)DBNull.Value : inspectData.PcbBarcode);
cmd.Parameters.AddWithValue("@Maker", maker); cmd.Parameters.AddWithValue("@Maker", maker);
cmd.Parameters.AddWithValue("@Model", model); cmd.Parameters.AddWithValue("@Model", model);
cmd.Parameters.AddWithValue("@Variant_1", variant1); cmd.Parameters.AddWithValue("@Variant_1", variant1);

74
leak_test_project/Services/TestProcessService.cs

@ -285,7 +285,7 @@ namespace leak_test_project.Services
{ {
TestIndex = testIndex, TestIndex = testIndex,
SensorData = failedSensorData, SensorData = failedSensorData,
PcbBarcode = "-", PcbBarcode = null,
MeasuredValue = "0.0000", MeasuredValue = "0.0000",
Judgment = "NG", Judgment = "NG",
SensorJudgment = "FAIL", SensorJudgment = "FAIL",
@ -302,9 +302,13 @@ namespace leak_test_project.Services
string pcbBarcode = null; string pcbBarcode = null;
var dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath(); var dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath();
var loginSettings = Housing.Services.LoginSettings.Load(dbIniPath); var loginSettings = Housing.Services.LoginSettings.Load(dbIniPath);
var barcodeSettings = Housing.Services.BarcodeSettings.Load();
bool isBarcodeRequired = barcodeSettings.BarcodeMapping;
if (loginSettings.OfflinePreview) if (loginSettings.OfflinePreview)
{ {
pcbBarcode = "testPCBbarcode"; // 오프라인 모드에서는 DB 조회를 건너뜀 (더미 데이터 생성 금지)
pcbBarcode = null;
} }
else else
{ {
@ -319,7 +323,40 @@ namespace leak_test_project.Services
catch (Exception ex) catch (Exception ex)
{ {
string detailedErr = $"바코드 매핑 실패: [{ex.GetType().Name}] {ex.Message}"; string detailedErr = $"바코드 매핑 실패: [{ex.GetType().Name}] {ex.Message}";
NotifyError(testIndex, detailedErr); FileLogger.Log("WARN", $"[TestProcess-{side}] {detailedErr}");
if (isBarcodeRequired)
{
NotifyError(testIndex, detailedErr);
_dioBoard.WriteOutput(side + "_NG", true);
var failedSensorData = new SensorIdData { ID = sensorData.ID };
TestCompleted?.Invoke(this, new TestResultEventArgs
{
TestIndex = testIndex,
SensorData = failedSensorData,
PcbBarcode = null,
MeasuredValue = "0.0000",
Judgment = "NG",
SensorJudgment = "FAIL",
SpecMismatch = false,
ProductionDate = DateTime.Now
});
NotifyProgress(testIndex, "시험 완료", "LightBlue");
continue;
}
else
{
pcbBarcode = null;
}
}
}
if (string.IsNullOrEmpty(pcbBarcode))
{
if (isBarcodeRequired)
{
NotifyError(testIndex, "바코드 매핑 실패: 매핑 데이터 없음");
_dioBoard.WriteOutput(side + "_NG", true); _dioBoard.WriteOutput(side + "_NG", true);
var failedSensorData = new SensorIdData { ID = sensorData.ID }; var failedSensorData = new SensorIdData { ID = sensorData.ID };
@ -327,7 +364,7 @@ namespace leak_test_project.Services
{ {
TestIndex = testIndex, TestIndex = testIndex,
SensorData = failedSensorData, SensorData = failedSensorData,
PcbBarcode = "-", PcbBarcode = null,
MeasuredValue = "0.0000", MeasuredValue = "0.0000",
Judgment = "NG", Judgment = "NG",
SensorJudgment = "FAIL", SensorJudgment = "FAIL",
@ -337,32 +374,17 @@ namespace leak_test_project.Services
NotifyProgress(testIndex, "시험 완료", "LightBlue"); NotifyProgress(testIndex, "시험 완료", "LightBlue");
continue; continue;
} }
else
{
pcbBarcode = null;
}
} }
else
if (string.IsNullOrEmpty(pcbBarcode))
{ {
NotifyError(testIndex, "바코드 매핑 실패: 매핑 데이터 없음"); // 정상 매핑 시점 이벤트 발생
_dioBoard.WriteOutput(side + "_NG", true); BarcodeMapped?.Invoke(this, (testIndex, sensorData.ID, pcbBarcode));
var failedSensorData = new SensorIdData { ID = sensorData.ID };
TestCompleted?.Invoke(this, new TestResultEventArgs
{
TestIndex = testIndex,
SensorData = failedSensorData,
PcbBarcode = "-",
MeasuredValue = "0.0000",
Judgment = "NG",
SensorJudgment = "FAIL",
SpecMismatch = false,
ProductionDate = DateTime.Now
});
NotifyProgress(testIndex, "시험 완료", "LightBlue");
continue;
} }
// 정상 매핑 시점 이벤트 발생
BarcodeMapped?.Invoke(this, (testIndex, sensorData.ID, pcbBarcode));
// === 3단계: LEAK 시험 수행 === // === 3단계: LEAK 시험 수행 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 3: LEAK 시험 대기 시작 (최대 30초)"); Debug.WriteLine($"[TestProcess-{side}] Phase 3: LEAK 시험 대기 시작 (최대 30초)");
NotifyProgress(testIndex, "LEAK 시험중", "LightYellow"); NotifyProgress(testIndex, "LEAK 시험중", "LightYellow");

7
leak_test_project/Setting.ini

@ -0,0 +1,7 @@
; =====================================================================
; Leak Test System - 환경 및 바코드 설정 파일 (Setting.ini)
; =====================================================================
[Barcode]
; BarcodeMapping: 바코드 매핑 필수 여부 (true = 매핑 미될 시 검사 중단, false = 매핑 없어도 검사 진행)
BarcodeMapping=false

8
leak_test_project/ViewModels/HomeViewModel.cs

@ -289,12 +289,12 @@ namespace leak_test_project.ViewModels
if (args.TestIndex == 0) if (args.TestIndex == 0)
{ {
LeftIcSn = args.IcSn; LeftIcSn = args.IcSn;
LeftPcbBarcode = args.PcbBarcode; LeftPcbBarcode = string.IsNullOrEmpty(args.PcbBarcode) ? "-" : args.PcbBarcode;
} }
else else
{ {
RightIcSn = args.IcSn; RightIcSn = args.IcSn;
RightPcbBarcode = args.PcbBarcode; RightPcbBarcode = string.IsNullOrEmpty(args.PcbBarcode) ? "-" : args.PcbBarcode;
} }
}); });
@ -309,7 +309,7 @@ namespace leak_test_project.ViewModels
LeftStatus = "시험 완료"; LeftStatus = "시험 완료";
LeftStartTime = $"{_leftActualStartTime} / {endTimeStr}"; LeftStartTime = $"{_leftActualStartTime} / {endTimeStr}";
LeftIcSn = e.SensorData.ID; LeftIcSn = e.SensorData.ID;
LeftPcbBarcode = e.PcbBarcode; LeftPcbBarcode = string.IsNullOrEmpty(e.PcbBarcode) ? "-" : e.PcbBarcode;
} }
else else
{ {
@ -319,7 +319,7 @@ namespace leak_test_project.ViewModels
RightStatus = "시험 완료"; RightStatus = "시험 완료";
RightStartTime = $"{_rightActualStartTime} / {endTimeStr}"; RightStartTime = $"{_rightActualStartTime} / {endTimeStr}";
RightIcSn = e.SensorData.ID; RightIcSn = e.SensorData.ID;
RightPcbBarcode = e.PcbBarcode; RightPcbBarcode = string.IsNullOrEmpty(e.PcbBarcode) ? "-" : e.PcbBarcode;
} }
// SPEC 교차 검증 불일치 경고 // SPEC 교차 검증 불일치 경고

6
leak_test_project/Views/ParametersWindow.xaml

@ -89,6 +89,12 @@
<TextBox x:Name="editSpecLL" Text="-1.00" Style="{StaticResource ParamTextBox}"/> <TextBox x:Name="editSpecLL" Text="-1.00" Style="{StaticResource ParamTextBox}"/>
</Border> </Border>
</StackPanel> </StackPanel>
<!-- BARCODE MAPPING OPTION -->
<TextBlock Text="[ 바코드 검사 설정 ]" FontSize="15" FontWeight="Bold" Foreground="#2E86C1" Margin="0,20,0,12" HorizontalAlignment="Left"/>
<StackPanel Orientation="Horizontal" Margin="0,5">
<CheckBox x:Name="chkEnableBarcodeMapping" Content="바코드 매핑 필수" FontSize="14" FontWeight="Bold" Foreground="#2C3E50" VerticalAlignment="Center"/>
</StackPanel>
</StackPanel> </StackPanel>
</Grid> </Grid>
</ScrollViewer> </ScrollViewer>

15
leak_test_project/Views/ParametersWindow.xaml.cs

@ -1,5 +1,6 @@
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using Housing.Services;
using leak_test_project.Utils; using leak_test_project.Utils;
namespace leak_test_project.Views namespace leak_test_project.Views
@ -15,6 +16,9 @@ namespace leak_test_project.Views
editSpecUL.Text = config.SpecUL.ToString("F2"); editSpecUL.Text = config.SpecUL.ToString("F2");
editSpecLL.Text = config.SpecLL.ToString("F2"); editSpecLL.Text = config.SpecLL.ToString("F2");
// 바코드 설정 로드
var barcodeSettings = BarcodeSettings.Load();
chkEnableBarcodeMapping.IsChecked = barcodeSettings.BarcodeMapping;
} }
private void BtnSave_Click(object sender, RoutedEventArgs e) private void BtnSave_Click(object sender, RoutedEventArgs e)
@ -29,11 +33,16 @@ namespace leak_test_project.Views
ConfigManager.Current.SpecLL = ll; ConfigManager.Current.SpecLL = ll;
} }
ConfigManager.Save(); ConfigManager.Save();
// 바코드 매핑 설정 저장
var barcodeSettings = new BarcodeSettings
{
BarcodeMapping = chkEnableBarcodeMapping.IsChecked == true
};
BarcodeSettings.Save(barcodeSettings);
MessageBox.Show("파라미터 설정이 저장되었습니다."); MessageBox.Show("파라미터 및 바코드 설정이 저장되었습니다.");
this.DialogResult = true; this.DialogResult = true;
this.Close(); this.Close();
} }

3
leak_test_project/leak_test_project.csproj

@ -38,6 +38,9 @@
<None Update="Database.ini"> <None Update="Database.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>
<None Update="Setting.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Sensor.ini"> <None Update="Sensor.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> </None>

1
ob/01_System_Architecture.md

@ -57,6 +57,7 @@ graph TD
FL["FileLogger (Local CSV Backup)"] FL["FileLogger (Local CSV Backup)"]
DS["IDialogService (Message Popups)"] DS["IDialogService (Message Popups)"]
Config["ConfigManager (Settings)"] Config["ConfigManager (Settings)"]
BS["BarcodeSettings (Setting.ini)"]
end end
end end

2
ob/02_DB_Design_ERD.md

@ -35,7 +35,7 @@
| 컬럼명 | 데이터 타입 | Null 여부 | 제약조건 | 설명 | | 컬럼명 | 데이터 타입 | Null 여부 | 제약조건 | 설명 |
| :--- | :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- | :--- |
| **IC_SN** | `VARCHAR(50)` | NOT NULL | PK | 센서에서 판독한 16자리 제품 고유 시리얼 번호 | | **IC_SN** | `VARCHAR(50)` | NOT NULL | PK | 센서에서 판독한 16자리 제품 고유 시리얼 번호 |
| **PCB_Barcode** | `VARCHAR(50)` | NOT NULL | - | 인쇄 회로 기판(PCB) 바코드 (식별 불가 시 `-` 처리) | | **PCB_Barcode** | `VARCHAR(50)` | NULL | Filtered UK | 인쇄 회로 기판(PCB) 바코드 (식별 불가/미존재 시 `NULL` 처리) |
| **Maker** | `NVARCHAR(50)` | NULL | - | 제조사 식별 정보 | | **Maker** | `NVARCHAR(50)` | NULL | - | 제조사 식별 정보 |
| **Model** | `NVARCHAR(50)` | NULL | - | 검사 대상 모델 코드 | | **Model** | `NVARCHAR(50)` | NULL | - | 검사 대상 모델 코드 |
| **Variant_1** | `NVARCHAR(50)` | NULL | - | 파생 기종 식별 속성 1 | | **Variant_1** | `NVARCHAR(50)` | NULL | - | 파생 기종 식별 속성 1 |

36
ob/05_Implementation_History.md

@ -244,3 +244,39 @@
15. **데이터 조회 문단 줄바꿈 교정**: `06_데이터_조회.md` 상단 서술 문장 간 및 `## 6.1 화면 구성` 헤더 사이의 공백 줄(줄바꿈)을 정돈함. 15. **데이터 조회 문단 줄바꿈 교정**: `06_데이터_조회.md` 상단 서술 문장 간 및 `## 6.1 화면 구성` 헤더 사이의 공백 줄(줄바꿈)을 정돈함.
16. **로그 관리 타임아웃 측정값 오기 수정**: `08_로그_관리.md`에서 타임아웃 발생 시 오기 기재되어 있던 측정값 `0.0000`을 실제 소스 코드 동작값인 `9.9999` (판정 `NG`, 센서 판정 `T/O`)로 올바르게 수정함. 16. **로그 관리 타임아웃 측정값 오기 수정**: `08_로그_관리.md`에서 타임아웃 발생 시 오기 기재되어 있던 측정값 `0.0000`을 실제 소스 코드 동작값인 `9.9999` (판정 `NG`, 센서 판정 `T/O`)로 올바르게 수정함.
17. **로그 관리 페이지 분리**: `08_로그_관리.md`의 `8.5 데이터베이스 데이터 관리 (dbo.EOL)` 단락 앞에 `<div class="page-break"></div>` 태그를 삽입하여 다음 페이지로 독립 배치되도록 개선함. 17. **로그 관리 페이지 분리**: `08_로그_관리.md`의 `8.5 데이터베이스 데이터 관리 (dbo.EOL)` 단락 앞에 `<div class="page-break"></div>` 태그를 삽입하여 다음 페이지로 독립 배치되도록 개선함.
### 📅 2026-08-04 (4251 보드 IC_SN 읽기 로직 및 통신 타임아웃 개선)
* **내용**: 4251 보드 통신 서비스(`Board4251.cs`)에서 `IC_SN` 읽기 시 발생하던 락(SemaphoreSlim) 블로킹 타임아웃, 수신 패킷 조기 완료 판단 오작동, 가변 시리얼 길이 파싱 제한 문제를 전면 재점검하고 개선하였습니다.
* **기능**:
1. **초기화 커맨드 락 블로킹 해결**: `Board4251SensorService.ReadSensorAsync()` 내 `_ = _service.SendCommandAsync("x00o\r\n");`를 `await _service.SendCommandAsync("x00o\r\n");`로 변경하여, `SemaphoreSlim _lock`이 해제되지 않아 뒤이어 호출되는 상태 확인 및 ID 읽기 명령이 5초 이상 대기 후 타임아웃(Timeout)되는 현상을 근본적으로 해결함.
2. **조기 수신 완료 정교화**: `Board4251Service.OnDataReceived`에서 명령어 에코가 아닌 독립된 응답 라인에서 시리얼 ID를 수신하였을 때만 완료 처리되도록 로직을 정밀 조정하여 불완전 패킷 조기 취득 현상을 예방함.
3. **가변 시리얼 ID 정규식 유연화**: `ExtractId` 파싱 정규식을 16자리 정확 매칭(`\b[A-Za-z0-9]{16}\b`) 우선 판단 후 8~32자리 범위 정규식(`[A-Za-z0-9\-_]{8,32}`)을 지원하도록 개선하여 다양한 규격의 바코드가 안정적으로 파싱되도록 반영함.
### 📅 2026-08-12 (바코드 미매핑 시 검사 진행 허용 설정 및 더미 데이터 제거)
* **내용**: 바코드 매핑에 실패하더라도 검사를 중단하지 않고 계속 진행할 수 있도록 `Setting.ini` 설정 파일에 `BarcodeMapping` 옵션을 신설하고, `Database.ini`와의 중복 없이 `Setting.ini` 오직 한 곳에서만 바코드 매핑 옵션을 관리하도록 확립하였으며, 바코드가 없을 시 더미 바코드 문자열(`"testPCBbarcode"`)이 생성/유입되는 문제를 근본적으로 해결했습니다.
* **기능**:
1. **`BarcodeSettings.cs` 클래스 신설 및 설정 파싱**:
- `Setting.ini` 파일로부터 `BarcodeMapping` (기본값: `true`) 설정을 전용 로드/파싱하는 `BarcodeSettings` 클래스를 구현했습니다.
- `Setting.ini` 파일이 존재하지 않는 경우 주석 설명과 함께 기본 설정 파일이 자동 생성되도록 `EnsureDefaultFile` 및 `App.xaml.cs` 시작 시점 연동을 구축했습니다.
2. **바코드 미매핑 시 검사 유연화 (`TestProcessService.cs`)**:
- `BarcodeMapping=true` (기본값): 바코드 매핑 실패 또는 매핑 데이터 미존재 시 기존과 동일하게 오류 알림, NG 디지털 출력 및 검사 중단(`continue`).
- `BarcodeMapping=false`: 바코드 매핑에 실패하거나 바코드 데이터가 없어도 오류 로깅 후 검사를 중단하지 않고 `pcbBarcode = string.Empty`로 유지한 채 3단계 LEAK 검사 단계로 정상 진입하여 검사를 계속 수행합니다.
3. **더미 바코드 데이터 일절 생성 금지**:
- 오프라인 모드(`loginSettings.OfflinePreview`) 또는 바코드 미매핑 상황에서 `"testPCBbarcode"` 등 임의의 더미 문자열을 생성하여 데이터베이스나 CSV 로그, 이벤트 등에 전달하던 구문을 완전히 제거하고 빈값(`""`)으로 처리하도록 일원화하였습니다.
4. **시험 설정 UI(`ParametersWindow`)에 ON/OFF 체크박스 연동**:
- 프로그램 하단 메뉴의 **Parameters (시험 설정)** 창에 `바코드 매핑 필수 (미체크 시 바코드 없이 검사 진행)` 체크박스(`chkEnableBarcodeMapping`)를 신설하고, 설정 변경 후 '설정 저장' 버튼 클릭 시 `Setting.ini`에 즉시 기록되도록 UI 컨트롤 및 비하인드 코드를 연동하였습니다.
### 📅 2026-08-13 (바코드 미존재 시 NULL 처리 적용)
* **내용**: 바코드가 존재하지 않거나 바코드 매핑에 실패하였거나, 바코드 미체크 상태인 경우 기존의 `"-"` 또는 `""`(빈 문자열) 대신 `NULL`(`DBNull.Value`)로 데이터베이스 및 모델 데이터를 처리하도록 보완하였습니다.
* **기능**:
1. **DB 스키마 NULL 가능 및 Filtered Unique Index 변경 (`dbo.EOL_Schema.sql`)**:
- `[dbo].[EOL]` 테이블의 `[PCB_Barcode]` 컬럼 제약조건을 `NOT NULL`에서 `NULL` 허용으로 개편하였습니다.
- NULL 값이 다수 입력되더라도 UNIQUE 제약 위반이 발생하지 않도록 `Filtered Unique Index` (`WHERE PCB_Barcode IS NOT NULL`)로 인덱스 구조를 개선하였습니다.
2. **DB 파라미터 NULL 바인딩 (`EolDatabaseService.cs`)**:
- `inspectData.PcbBarcode`가 비어있거나 null 또는 `"-"`인 경우 SQL `@PCB_Barcode` 파라미터 값에 `(object)DBNull.Value`를 지정하도록 수정하였습니다.
3. **데이터 모델 및 프로세스 서비스 null 세팅 (`ProjectModels.cs`, `TestProcessService.cs`)**:
- `InspectData.PcbBarcode` 기본값을 `null`로 지정하고, 바코드 매핑 실패 또는 미사용 시 `TestCompleted` 이벤트 및 데이터 전송 시 `PcbBarcode = null`을 부여하였습니다.
4. **HomeViewModel UI 폴백 보완 (`HomeViewModel.cs`)**:
- 메인 화면에 바코드 렌더링 시 null 값이 유입될 경우 UI 레이아웃 정합성을 보장하기 위해 `"-"`로 폴백 표현되도록 방어 처리하였습니다.

4
ob/db_login_integration.md

@ -88,7 +88,7 @@
| 물리 컬럼명 | 데이터 타입 | 매핑 설명 | 비고 | | 물리 컬럼명 | 데이터 타입 | 매핑 설명 | 비고 |
| :--- | :--- | :--- | :--- | | :--- | :--- | :--- | :--- |
| **`IC_SN`** | `NVARCHAR(10)` | 제품의 최종 조합 ID (`inspectData.IcSn`) | **PK / FK** | | **`IC_SN`** | `NVARCHAR(10)` | 제품의 최종 조합 ID (`inspectData.IcSn`) | **PK / FK** |
| **`PCB_Barcode`** | `NVARCHAR(10)` | 고유 식별용 바코드 정보 | **UK** *(inspectData.PcbBarcode 매핑)* | | **`PCB_Barcode`** | `NVARCHAR(10)` | 고유 식별용 바코드 정보 *(바코드 미존재 시 `NULL` / `DBNull.Value` 처리)* | **Filtered UK** *(inspectData.PcbBarcode 매핑)* |
| `Maker` | `NVARCHAR(10)` | 로그인 메이커 정보 (`App.LoginResult.Maker`) | | | `Maker` | `NVARCHAR(10)` | 로그인 메이커 정보 (`App.LoginResult.Maker`) | |
| `Model` | `NVARCHAR(10)` | 로그인 모델 정보 (`App.LoginResult.Model`) | | | `Model` | `NVARCHAR(10)` | 로그인 모델 정보 (`App.LoginResult.Model`) | |
| `Variant_1` | `NVARCHAR(10)` | 로그인 변형1 정보 (`App.LoginResult.Variant1`) | | | `Variant_1` | `NVARCHAR(10)` | 로그인 변형1 정보 (`App.LoginResult.Variant1`) | |
@ -175,7 +175,7 @@ WHEN NOT MATCHED THEN
1. **1.5단계 바코드 매핑 프로세스**: 센서 ID(IC_SN)를 판독한 후 2단계 릭 계측 대기로 진입하기 전 데이터베이스의 `dbo.Housing_Assembly`를 조회합니다. 이 때 연결 지연을 극복하기 위해 연결 제한 시간(`ConnectTimeout`)을 **5초**로 지정합니다. 바코드 조회가 성공하면 즉시 메인 화면에 **`IC_SN / PCB_Barcode`** 형태로 실시간 결합 렌더링됩니다. 1. **1.5단계 바코드 매핑 프로세스**: 센서 ID(IC_SN)를 판독한 후 2단계 릭 계측 대기로 진입하기 전 데이터베이스의 `dbo.Housing_Assembly`를 조회합니다. 이 때 연결 지연을 극복하기 위해 연결 제한 시간(`ConnectTimeout`)을 **5초**로 지정합니다. 바코드 조회가 성공하면 즉시 메인 화면에 **`IC_SN / PCB_Barcode`** 형태로 실시간 결합 렌더링됩니다.
2. **오프라인 미리보기 모드(OfflinePreview)**: `OfflinePreview` 설정이 `true`로 활성화되어 있을 경우, 실제 데이터베이스 연결을 생략하고 지정된 테스트 바코드 상수값인 **`"testPCBbarcode"`**를 `PCB_Barcode` 데이터로 주입하여 시험을 정상적으로 이어나갑니다. 2. **오프라인 미리보기 모드(OfflinePreview)**: `OfflinePreview` 설정이 `true`로 활성화되어 있을 경우, 실제 데이터베이스 연결을 생략하고 지정된 테스트 바코드 상수값인 **`"testPCBbarcode"`**를 `PCB_Barcode` 데이터로 주입하여 시험을 정상적으로 이어나갑니다.
3. **조회 예외 및 데이터 누락 대응**: DB 통신 장애로 인한 예외(SqlException 등)가 발생하거나 매핑 정보가 존재하지 않을 경우, 즉시 화면에 `"바코드 매핑 실패: [ExceptionType] ExceptionMessage"` 또는 `"바코드 매핑 실패: 매핑 데이터 없음"` 오류를 출력하고 기밀 시험을 즉각 중단합니다. 3. **조회 예외 및 데이터 누락 대응**: DB 통신 장애로 인한 예외(SqlException 등)가 발생하거나 매핑 정보가 존재하지 않을 경우, 즉시 화면에 `"바코드 매핑 실패: [ExceptionType] ExceptionMessage"` 또는 `"바코드 매핑 실패: 매핑 데이터 없음"` 오류를 출력하고 기밀 시험을 즉각 중단합니다.
4. **물리 출력 및 정합성 보장**: 조회가 실패하여 중단된 장치는 물리 NG 접점(`side_NG`)을 출력 처리하고, `TestCompleted` 이벤트 및 CSV 결과 로그에 `PcbBarcode = "-"` 및 판정 결과 `NG`를 이중 기록하여 무결성을 유지합니다. 4. **물리 출력 및 정합성 보장**: 조회가 실패하여 중단된 장치는 물리 NG 접점(`side_NG`)을 출력 처리하고, `TestCompleted` 이벤트 및 CSV 결과 로그에 `PcbBarcode = null` 및 판정 결과 `NG`를 이중 기록하며 DB에는 `DBNull.Value`로 처리하여 무결성을 유지합니다.
5. **ID 판독 실패 시 안전 제어 및 크래시 방지**: 시험 기동 신호 유입 시 ID 센서로부터 ID 값(IC_SN)을 판독하는 데 실패한 경우(`sensorData == null`), 즉시 화면에 `"ID 읽기 실패"` 오류를 출력하고 물리 NG 접점(`side_NG`)을 출력 및 결과 이벤트를 전송한 후, `continue` 제어문을 통해 시험 시퀀스를 즉각 중단하고 다음 시험 대기 상태로 복귀하도록 구현하여 `NullReferenceException`에 의한 프로그램 비정상 종료(크래시) 현상을 완벽히 방지하였습니다. 5. **ID 판독 실패 시 안전 제어 및 크래시 방지**: 시험 기동 신호 유입 시 ID 센서로부터 ID 값(IC_SN)을 판독하는 데 실패한 경우(`sensorData == null`), 즉시 화면에 `"ID 읽기 실패"` 오류를 출력하고 물리 NG 접점(`side_NG`)을 출력 및 결과 이벤트를 전송한 후, `continue` 제어문을 통해 시험 시퀀스를 즉각 중단하고 다음 시험 대기 상태로 복귀하도록 구현하여 `NullReferenceException`에 의한 프로그램 비정상 종료(크래시) 현상을 완벽히 방지하였습니다.
6. **메인 화면 식별자 행 분리 및 마진 최적화**: 좌/우 채널 모니터링 테이블 내부의 식별자 정보를 가로 텍스트 잘림 현상 예방 및 가독성 강화를 위해 기존 `"ID"` 행에서 **`"IC_SN"`**과 **`"PCB_Barcode"`**의 **2개 독립된 행(Rows)으로 분할**하여 전면 배치했습니다. 이를 통해 고정 헤더 가로폭을 확장하지 않고 기존의 깔끔한 가로 너비인 **`130`**을 안정적으로 유지합니다. 더불어 영문 `"PCB_Barcode"` 라벨 텍스트의 좌우 여백을 개별 마진 **`Margin="6,0"`**으로 미세 조율하여, 윈도우의 가로 크기를 최소 사양인 **`1150`**으로 줄인 창모드 상태에서도 단 1픽셀의 잘림 없이 모든 텍스트가 명확하게 표현되도록 방어했습니다. 6. **메인 화면 식별자 행 분리 및 마진 최적화**: 좌/우 채널 모니터링 테이블 내부의 식별자 정보를 가로 텍스트 잘림 현상 예방 및 가독성 강화를 위해 기존 `"ID"` 행에서 **`"IC_SN"`**과 **`"PCB_Barcode"`**의 **2개 독립된 행(Rows)으로 분할**하여 전면 배치했습니다. 이를 통해 고정 헤더 가로폭을 확장하지 않고 기존의 깔끔한 가로 너비인 **`130`**을 안정적으로 유지합니다. 더불어 영문 `"PCB_Barcode"` 라벨 텍스트의 좌우 여백을 개별 마진 **`Margin="6,0"`**으로 미세 조율하여, 윈도우의 가로 크기를 최소 사양인 **`1150`**으로 줄인 창모드 상태에서도 단 1픽셀의 잘림 없이 모든 텍스트가 명확하게 표현되도록 방어했습니다.
7. **설정 파일 통합 및 C드라이브 배포 자동화**: 7. **설정 파일 통합 및 C드라이브 배포 자동화**:

Loading…
Cancel
Save