From 233c28ff25f982caaf5728c909cdfb846a60dca3 Mon Sep 17 00:00:00 2001
From: seuuung
Date: Tue, 18 Aug 2026 09:08:43 +0900
Subject: [PATCH] =?UTF-8?q?NG=20=EA=B2=B0=EA=B3=BC=20Db=20=EC=A0=80?=
=?UTF-8?q?=EC=9E=A5=20=EB=B0=8F=20=EC=B9=B4=EC=9A=B4=ED=8A=B8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.vs/marking_gui.slnx/v18/.suo | Bin 92160 -> 94720 bytes
.../v18/DocumentLayout.backup.json | 12 +-
.vs/marking_gui.slnx/v18/DocumentLayout.json | 12 +-
marking_gui/MainWindow.xaml | 2 +-
marking_gui/MainWindow.xaml.cs | 128 ++++--
marking_gui/Manual/.obsidian/workspace.json | 4 +-
marking_gui/Manual/07_시험_프로세스.md | 14 +-
marking_gui/Manual/Manual_Merged.md | 30 +-
marking_gui/Manual/Manual_for_pdf.html | 52 +--
marking_gui/Models/MarkingResult.cs | 18 +
marking_gui/PortSettingsWindow.xaml | 1 -
marking_gui/PortSettingsWindow.xaml.cs | 50 ---
marking_gui/QrVerificationWindow.xaml.cs | 16 +-
marking_gui/Services/Board4251Service.cs | 13 +
marking_gui/Services/EzdMarkingService.cs | 390 +++++++++---------
marking_gui/Services/MarkingConfigService.cs | 24 ++
marking_gui/Services/SerialBarcodeScanner.cs | 122 +++++-
marking_gui/Setting.ini | 5 +-
marking_gui/SettingsWindow.xaml.cs | 4 +-
test_bukuk.ezd | Bin 0 -> 445633 bytes
20 files changed, 523 insertions(+), 374 deletions(-)
create mode 100644 test_bukuk.ezd
diff --git a/.vs/marking_gui.slnx/v18/.suo b/.vs/marking_gui.slnx/v18/.suo
index 67c01aa6d83c46c7bf6a1a26766969d860276b65..fb1a699b2da8ae57fa475d2fba832f65f6a28b01 100644
GIT binary patch
delta 3091
zcmchYe{54#6vuns+U^H!En5ehtfSzVgRSdp>Bc&7KE?zAog0j0Q+{<_%a0J)U_*tB
zKG7lS0<0(DDSse5lW9bB>r+a|gaY!9kTC?27?6RQj6XDLm{H<3&mC_O5Cl#1HlLhx
zf4uwdJKuXw#crMWt!_IHT&r`8E=Hr#@Ezr2IAeSJxy*v>i;zhpBEGw6b6NyQLlVH#8B^yzFb+1A9O?~W6%ZM+cfyW2|`_sli6`t14G*?sq8RA)j
zodWBumS7Ead_*nbCjvx}SVk--RuC%*_NG;;T}`%*SgZEg+;cbi3&?+-Sg($4Alsz+
z8_BYlCz743S&cE=a^r5tcX!ZW1cc&JhGby-v&Lk|CYU6wZzjuRV7`v*%`&hthdOt=
z3<)&O&M-oqXELz$Oa^AHWdBx%hiJjD3@m^X2&N;GVOZb0$q>9
zDgGl>#@$nvP=Z_G6HY?NpjuM{_>@LdDQ5(Br-s9Zt*IHxHM1B4er&V^p%(irFL5Rl
zu20?^G9SC+;-@)07URT*q!z4AGb`q_27ns$rX}GU)@64Xl1FhNTLV0-G}>N=aC#kB
zF476UCN2>y)(m`kY?by`8o-{hwopcifhow6jm4226B{x_FkoNCDFYLhUB2Nd!wAr5
zSW>fwrv)oo-EW6-1=#_i%n}1jXSS3zwiEPVV`dNn*q1pNzmz5iSfNn*t!wY&XmSo0
z%9@VTQyx{stZTq=fa@l0gx|1l(kc8jhSy)Fy$)!V>mEHP^PHT|$t7I4;20Qzy`NQs
z;dAH*sB>O}*B_px?k9p=#G$u0RjQr??@0U{n276p6Y$_62P(ZS_=Av$(OD&}e)C9n
zU)e^iy-NOAst-w!!>UAUU5<61I{g9J4~Y)7ZztQ|>P>?2H&$K|+yKM(q?fm#uU@be4wa1a-6
zv~k85CAhIikMmxCLHWG-MIF>*>!#UgD>va=ElK(({L7avYfw(?<#e%ocOT5vo_!O~
zwr_|%o?2XZKKC`H^5gdm;Z+OFp!}oV^0er5Prz+QGnF|%4cYA$P`~{vsQ^|4UyPG4%i7+=7
zw_b6fcqJ=kIjaB~{nS;BD_0gVms;`k!UZT?U54ewDHy#Xwz(mW-L%Kiyk!KIwKv@<
za9G*#p8Pw9KSffMl>NOR59s9s+O|00^(kN&(#qM|IK@J&>Sx&^qks)ax8)=Ay5z{{U;;Te>`LT(g4I+S;ptddW1
z`=0=|?8zw0gNQr}7IAkeFVlo@*EaYsWIF@56&sfRbERGLAzKp5Ad1sA*|6tn8+P^9
z!(zI#rT3A~xM!vXjbDDRz?q7e|NmN8+DyQ;l@b=v?yrI@klGWWQu;a#(%~cY{^Yxx
zg!h(hSThmpTXHeCJP~_l+9+g8u+CwQm@B{uQsQjzN7`n=PQc5(i&!jcq_PTFPk$T7
z@$l7Tthk!9BOL@uOoym6emwK6AWDLjisX_mtyE?MQzX#_AHco-ccuyNM8^RafdC8w
g4Bn>YeH7N=GA!+!4~~DY_>Nk-%9_ZFwGf0q086<-i~s-t
delta 3252
zcmc(he{56N6~}#UY$urK;3m#6WCVjvNFjFpE4E<_oF)rxRzje(0%igsPS&YoHN;GAIhV&YuSUQtX+}fw>1-sI#;M|Hf;@6sZ>>>q|*Ka;6;eD6K?-23kPo_Ei+{8xi~$#BdAx^8c=0dzW@vv)(WFw2e?9xywvK!7X}
z>=64v(nf>>I*Z9LhnP>yC5*&aw%D@ml5B0IdKQsK6cCGv?-Gj$J@FmFM2ymWJJq~Q
zVYV)!I+s{NEF-Sd*cqO}PQM}p*S%D~z-{!J9B@BEd0}@vfVSdlxOcp>>b6~3-`&}o
z=W*FX{Y2K*Ms){qDBI`p{gc`Usg1+XZr1(n`LD`?BVthUOmfEVxlSC;b!MdEl_VNu
zskqJ3S%`eXK(JIS;Y?C3pfR4uQgJ;Y)okjsRHXz<#bYcL*IY=df{ZjQ6>G;*u}n*+
zmub_tB~#|<*`K5-poL8X&>hz&r7xTL3D~K%<<|+kD7@ORMb3c_u+7>DS8&Aou()c*
zy+w!B5|Ol2;l;o@JZAA{Jtas6$tJv*2-Zs>dL?J}omq5kI4A+e=36w&`~+B8DkerQ
z%|r`Q53PudeuqqW%|hg8W$z^b6v)JHyDd@OW0>ZpQ6$3yd7Kp{_JPCUK{IfV-)F*V;Dlk-KKp*IP;S}=e!S3ii_*Fcy6}8u
z0S>jVz)$wr@QFL_!*pdttrcom;N==ipG5)+*J}fHH0Jx-nbod`=rJ5mE{|rP&+W%2
zwYlF#{cg??ok?mTah!OPI6)}HJmS~HDdK738R8ejv&3`6Y2ufJN<2@TB^*Q#agJC@
zyhxlRULtr`FH?PSFeS%!*8tD@(H~ZISy8(wi%Wg_?ppSPP9U6k3I&|
z$b=uh>OoJA`{ZKR0OW
z+f4>|fIK(hm9`R;Wux}z2PuUei&|jAx3nA>a$lc!>&o}sNAU7P`*L48eZj52{1o2(
z@QW$=FJihejm9Lbzhem=O_lP_wT-C*xp?XKp45e-hLW8@SBu~0bp?DYnp``Zd_k8#
z$2gH7B2$)Oyz
zU-Z;~5k;Lm1If`*WwZfqJ+XssJ9#_EN~D2WXEWUfNPl7Pi|l|jC_A=;
zz0V$lAxK7TIrIlj2{pq)VmE&7r*B5^Ql&w){d8HQ0a`5z3A3pn^ay)!}?>CTYRSxN+bjIl}nwo*J
zJu2h=ts%O%LbLpVaJjc4xW5MP+txi@zMk0|&s}pW&NxJ2PT#>ebV47rz!<>Te*wuL
Ba)1B;
diff --git a/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json b/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json
index f151ae1..22dca5d 100644
--- a/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json
+++ b/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json
@@ -22,12 +22,16 @@
"DocumentGroups": [
{
"DockedWidth": 200,
- "SelectedChildIndex": 3,
+ "SelectedChildIndex": 4,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
+ {
+ "$type": "Bookmark",
+ "Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
+ },
{
"$type": "Document",
"DocumentIndex": 1,
@@ -37,8 +41,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeToolTip": "marking_gui\\QrVerificationWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
- "WhenOpened": "2026-07-29T04:12:09.69Z",
- "EditorCaption": ""
+ "WhenOpened": "2026-07-29T04:12:09.69Z"
},
{
"$type": "Document",
@@ -49,8 +52,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeToolTip": "marking_gui\\SettingsWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
- "WhenOpened": "2026-07-29T04:12:06.926Z",
- "EditorCaption": ""
+ "WhenOpened": "2026-07-29T04:12:06.926Z"
},
{
"$type": "Document",
diff --git a/.vs/marking_gui.slnx/v18/DocumentLayout.json b/.vs/marking_gui.slnx/v18/DocumentLayout.json
index f151ae1..22dca5d 100644
--- a/.vs/marking_gui.slnx/v18/DocumentLayout.json
+++ b/.vs/marking_gui.slnx/v18/DocumentLayout.json
@@ -22,12 +22,16 @@
"DocumentGroups": [
{
"DockedWidth": 200,
- "SelectedChildIndex": 3,
+ "SelectedChildIndex": 4,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
},
+ {
+ "$type": "Bookmark",
+ "Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
+ },
{
"$type": "Document",
"DocumentIndex": 1,
@@ -37,8 +41,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeToolTip": "marking_gui\\QrVerificationWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
- "WhenOpened": "2026-07-29T04:12:09.69Z",
- "EditorCaption": ""
+ "WhenOpened": "2026-07-29T04:12:09.69Z"
},
{
"$type": "Document",
@@ -49,8 +52,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeToolTip": "marking_gui\\SettingsWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
- "WhenOpened": "2026-07-29T04:12:06.926Z",
- "EditorCaption": ""
+ "WhenOpened": "2026-07-29T04:12:06.926Z"
},
{
"$type": "Document",
diff --git a/marking_gui/MainWindow.xaml b/marking_gui/MainWindow.xaml
index d4eb44c..a445187 100644
--- a/marking_gui/MainWindow.xaml
+++ b/marking_gui/MainWindow.xaml
@@ -449,7 +449,7 @@
-
+
diff --git a/marking_gui/MainWindow.xaml.cs b/marking_gui/MainWindow.xaml.cs
index 0d98f95..eb9bd08 100644
--- a/marking_gui/MainWindow.xaml.cs
+++ b/marking_gui/MainWindow.xaml.cs
@@ -355,7 +355,7 @@ namespace marking_gui
private void UpdateRangeGuides()
{
- txtVoltageRangeGuide.Text = string.Format("기준범위: {0:F1}V ~ {1:F1}V", _limits.MinVoltage, _limits.MaxVoltage);
+ txtVoltageRangeGuide.Text = string.Format("기준범위: {0:F2}V ~ {1:F2}V", _limits.MinVoltage, _limits.MaxVoltage);
txtCurrentRangeGuide.Text = string.Format("기준범위: {0:F2}mA ~ {1:F2}mA", _limits.MinCurrent, _limits.MaxCurrent);
txtResistanceRangeGuide.Text = string.Format("기준범위: {0:F3}kΩ ~ {1:F3}kΩ", _limits.MinResistance, _limits.MaxResistance);
}
@@ -1363,9 +1363,6 @@ namespace marking_gui
string lotNo = string.IsNullOrWhiteSpace(_loginInfo.LotNo) ? "00" : _loginInfo.LotNo.Trim();
string markedSerial = string.Format("{0}{1:D4}", lotNo, seq);
- string drawingNo = _markingConfig.DrawingNo;
- string hkmcNo = _markingConfig.HkmcNo;
- string qrString = string.Format("{0};{1};{2};{3};", serial, drawingNo, hkmcNo, markedSerial);
// 마킹 스캔 요청 데이터 빌드
var req = new MarkingRequest
@@ -1374,12 +1371,12 @@ namespace marking_gui
IcSerial = serial,
LotNo = lotNo,
ProductNo = _loginInfo.Model,
- DrawingNo = drawingNo,
- HkmcNo = hkmcNo,
+ DrawingNo = string.Empty,
+ HkmcNo = string.Empty,
EzdFileName = _currentEzdFile,
LineCode = _loginInfo.LineNo,
ShiftCode = "A", // 기본 1조
- QrString = qrString
+ QrString = string.Empty
};
MarkingResult markingRes;
@@ -1387,12 +1384,19 @@ namespace marking_gui
{
// 오프라인 모드 시뮬레이션
await System.Threading.Tasks.Task.Delay(500);
+ string offDrawingNo = _markingConfig.GetDrawingNoForEzd(_currentEzdFile);
+ string offHkmcNo = _markingConfig.GetHkmcNoForEzd(_currentEzdFile);
+ string offQr = string.Format("{0};{1};{2};{3};", serial, offDrawingNo, offHkmcNo, markedSerial);
+
markingRes = new MarkingResult
{
Success = true,
ErrorCode = 0,
ErrorMessage = "오프라인 모드 각인 완료 모사",
- MarkedAt = DateTime.Now
+ MarkedAt = DateTime.Now,
+ DrawingNo = offDrawingNo,
+ HkmcNo = offHkmcNo,
+ QrString = offQr
};
}
else
@@ -1403,13 +1407,17 @@ namespace marking_gui
// UI 스레드로 안전하게 컴백
await Dispatcher.InvokeAsync(async () =>
{
+ string ezdDrawingNo = markingRes.DrawingNo;
+ string ezdHkmcNo = markingRes.HkmcNo;
+ string ezdQrString = markingRes.QrString;
+
if (txtMarkedSerial != null)
{
txtMarkedSerial.Text = markedSerial;
}
if (txtMarkingQr != null)
{
- txtMarkingQr.Text = qrString;
+ txtMarkingQr.Text = string.IsNullOrWhiteSpace(ezdQrString) ? "---" : ezdQrString;
}
bool finalMarkingPass = markingRes.Success;
@@ -1418,10 +1426,10 @@ namespace marking_gui
{
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 완료 (QR 검증 대기)";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber
- LoggerService.Info($"[마킹 성공] S/N: {serial}, 각인번호: {markedSerial} -> QR 검증 팝업 오픈");
+ LoggerService.Info($"[마킹 성공] S/N: {serial}, 각인번호: {markedSerial}, 도면품번: {ezdDrawingNo}, HKMC품번: {ezdHkmcNo} -> QR 검증 팝업 오픈");
// DB 저장 전 레이저 각인 QR 코드 스캔 검증 팝업 오픈 (시리얼 스캐너 서비스 인계)
- var qrVerifyWin = new QrVerificationWindow(qrString, _barcodeScanner)
+ var qrVerifyWin = new QrVerificationWindow(ezdQrString, _barcodeScanner)
{
Owner = this
};
@@ -1439,8 +1447,8 @@ namespace marking_gui
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
LoggerService.Info($"[QR 검증 성공] S/N: {serial}, 각인번호: {markedSerial} -> DB 저장 진행");
- // QR 검증 통과 시 최종 DB 저장 및 완료 처리
- await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, true, "OK", markedSerial, bypassPrevSteps);
+ // QR 검증 통과 시 최종 DB 저장 및 완료 처리 (EZD 추출 실시간 품번 포함)
+ await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, true, "OK", markedSerial, ezdDrawingNo, ezdHkmcNo, bypassPrevSteps);
}
else
{
@@ -1448,8 +1456,8 @@ namespace marking_gui
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
LoggerService.Warn($"[QR 검증 실패] S/N: {serial}, 각인번호: {markedSerial} -> DB NG 저장 진행");
- // 마킹 성공 후 QR 검증 불일치/취소 시 NG로 DB 저장
- await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", markedSerial, bypassPrevSteps);
+ // 마킹 성공 후 QR 검증 불일치/취소 시 NG로 DB 저장 (EZD 추출 실시간 품번 포함)
+ await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", markedSerial, ezdDrawingNo, ezdHkmcNo, bypassPrevSteps);
}
}
else
@@ -1526,8 +1534,12 @@ namespace marking_gui
}
else
{
- // 불합격인 경우 즉시 DB 저장 및 복구
- await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, isFinalPass, "NG", "", bypassPrevSteps);
+ // 불합격인 경우에도 Lot No 및 예정 각인번호/품번 정보를 포함하여 DB 저장 및 복구
+ string markedSerial = GetCurrentMarkedSerial();
+ string drawingNo = _markingConfig.GetDrawingNoForEzd(_currentEzdFile);
+ string hkmcNo = _markingConfig.GetHkmcNoForEzd(_currentEzdFile);
+
+ await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, isFinalPass, "NG", markedSerial, drawingNo, hkmcNo, bypassPrevSteps: bypassPrevSteps);
}
}
}
@@ -1586,7 +1598,38 @@ namespace marking_gui
}
}
- private async Task SaveAndFinalizeAsync(string serial, double voltage, double current, double resistance, bool isFinalPass, string result, string markedSerial, bool bypassPrevSteps = false)
+ private string GetCurrentMarkedSerial()
+ {
+ int seq = 1;
+ if (txtNextSequence != null)
+ {
+ if (Dispatcher.CheckAccess())
+ {
+ string rawText = txtNextSequence.Text;
+ string digitsOnly = System.Text.RegularExpressions.Regex.Replace(rawText ?? "", @"[^\d]", "");
+ if (int.TryParse(digitsOnly, out int parsedSeq) && parsedSeq > 0)
+ {
+ seq = parsedSeq;
+ }
+ }
+ else
+ {
+ Dispatcher.Invoke(() =>
+ {
+ string rawText = txtNextSequence.Text;
+ string digitsOnly = System.Text.RegularExpressions.Regex.Replace(rawText ?? "", @"[^\d]", "");
+ if (int.TryParse(digitsOnly, out int parsedSeq) && parsedSeq > 0)
+ {
+ seq = parsedSeq;
+ }
+ });
+ }
+ }
+ string lotNo = string.IsNullOrWhiteSpace(_loginInfo.LotNo) ? "00" : _loginInfo.LotNo.Trim();
+ return string.Format("{0}{1:D4}", lotNo, seq);
+ }
+
+ private async Task SaveAndFinalizeAsync(string serial, double voltage, double current, double resistance, bool isFinalPass, string result, string markedSerial, string drawingNo = "", string hkmcNo = "", bool bypassPrevSteps = false)
{
// 종료 시간 기록 및 UI 갱신
_testEndTime = DateTime.Now;
@@ -1608,6 +1651,9 @@ namespace marking_gui
_currentProduct.PcbBarcode = serial;
}
+ string finalDrawingNo = string.IsNullOrWhiteSpace(drawingNo) ? _markingConfig.DrawingNo : drawingNo;
+ string finalHkmcNo = string.IsNullOrWhiteSpace(hkmcNo) ? _markingConfig.HkmcNo : hkmcNo;
+
try
{
// 데이터베이스 저장 (비동기 스레드 격리로 UI 프리징 방지)
@@ -1631,8 +1677,8 @@ namespace marking_gui
_testEndTime,
markedSerial,
pcbBarcodeToSave, // 강제검사 시 시리얼 번호 사용
- _markingConfig.DrawingNo,
- _markingConfig.HkmcNo);
+ finalDrawingNo,
+ finalHkmcNo);
});
if (dbResult)
@@ -1745,11 +1791,18 @@ namespace marking_gui
{
borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
}
- }
- // 컨트롤 원상 복구 (재검사를 위해)
- txtSerial.IsEnabled = true;
- SetStartButtonReady();
+ _dbWasOffline = true;
+ SetStartButtonConnecting("⚠️ DB 연결 필요");
+ }
+ finally
+ {
+ txtSerial.IsEnabled = true;
+ if (!_dbWasOffline)
+ {
+ SetStartButtonReady();
+ }
+ }
}
// 화면 렌더링 완료 시 최초 로그인 팝업 구동
@@ -1835,6 +1888,11 @@ namespace marking_gui
{
// DB 재연결 감시 루프 중단
_dbWatcherCts?.Cancel();
+ _dbWatcherCts?.Dispose();
+
+ // 윈도우 타이머 정지 (WPF DispatcherTimer GC 메모리 누수 방지)
+ _clockTimer?.Stop();
+ _clockTimer = null;
// LMC 마킹 기계 무선/시리얼 연결 핸들 안전 해제 (재시작 시 LMC 장치 락 방지)
_markingService?.Shutdown();
@@ -1882,7 +1940,7 @@ namespace marking_gui
if (!_isInspectionRunning)
{
- Dispatcher.Invoke(() =>
+ await Dispatcher.InvokeAsync(() =>
{
SetStartButtonConnecting("⚠️ DB 연결 필요");
@@ -1906,7 +1964,7 @@ namespace marking_gui
if (!_isInspectionRunning)
{
- Dispatcher.Invoke(() =>
+ await Dispatcher.InvokeAsync(() =>
{
if (txtSystemStatus != null)
{
@@ -1921,11 +1979,6 @@ namespace marking_gui
await Dispatcher.InvokeAsync(() => RefreshNextSequence());
}
}
- else if (isNowConnected && !_dbWasOffline && !_isInspectionRunning)
- {
- // DB 연결 유지 상태에서도 DB 레코드 직접 변경/삭제 시 각인 순번 실시간 반영 (조용히 UI 갱신)
- await Dispatcher.InvokeAsync(() => RefreshNextSequenceSilently());
- }
}
catch (Exception ex)
{
@@ -2101,7 +2154,18 @@ namespace marking_gui
{
_markingConfig.SaveLastSelectedEzdFile(fullPath);
}
- LoggerService.Info($"EZD 도안 파일 선택 완료: {fileName} (절대경로: {fullPath})");
+
+ string drawingNo = string.Empty;
+ string hkmcNo = string.Empty;
+ if (_markingService is EzdMarkingService ezdService)
+ {
+ drawingNo = ezdService.GetTextFromEzd(_markingConfig.DrawingNoTag, fullPath);
+ hkmcNo = ezdService.GetTextFromEzd(_markingConfig.HkmcNoTag, fullPath);
+ }
+ if (string.IsNullOrWhiteSpace(drawingNo)) drawingNo = _markingConfig.GetDrawingNoForEzd(fullPath);
+ if (string.IsNullOrWhiteSpace(hkmcNo)) hkmcNo = _markingConfig.GetHkmcNoForEzd(fullPath);
+
+ LoggerService.Info($"EZD 도안 파일 선택 완료: {fileName} (절대경로: {fullPath}, 도면품번: {drawingNo}, HKMC품번: {hkmcNo})");
}
private void InitializeBarcodeScanner()
diff --git a/marking_gui/Manual/.obsidian/workspace.json b/marking_gui/Manual/.obsidian/workspace.json
index 3f3bab1..e323cef 100644
--- a/marking_gui/Manual/.obsidian/workspace.json
+++ b/marking_gui/Manual/.obsidian/workspace.json
@@ -41,7 +41,9 @@
"type": "file-explorer",
"state": {
"sortOrder": "alphabetical",
- "autoReveal": false
+ "autoReveal": false,
+ "showSearch": false,
+ "searchQuery": ""
},
"icon": "lucide-folder-closed",
"title": "파일 탐색기"
diff --git a/marking_gui/Manual/07_시험_프로세스.md b/marking_gui/Manual/07_시험_프로세스.md
index 335910b..fa876ef 100644
--- a/marking_gui/Manual/07_시험_프로세스.md
+++ b/marking_gui/Manual/07_시험_프로세스.md
@@ -67,8 +67,8 @@ graph TD
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, `NG` 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (OK)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(**`OK`**) 판정이 내려집니다.
- * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
- * 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
+ * 합격 판정과 동시에 선택된 EZD 도안 파일(.ezd)에서 도면품번 및 HKMC품번을 실시간으로 추출하여 각인 및 QR 코드가 자동으로 조합되어 레이저 마킹이 실행됩니다.
+ * 마킹 완료 후 EZD 파싱 품번 기반의 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증하고, 검증 완료 시 해당 품번 데이터로 DB 저장을 완료합니다.
---
@@ -76,12 +76,14 @@ graph TD
레이저 마킹이 완료되면 검증을 위해 **QR 검증 팝업 창**이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
-1. **QR 스캔 검증 성공 (즉시 자동 닫힘)**
+1. **QR 스캔 검증 성공 (즉시 자동 닫힘 및 DB OK 적재)**
- 스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 **즉시 자동으로 닫히며** DB에 `OK` 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
-2. **QR 스캔 검증 실패 (NG 판정 및 설명 표출)**
- - 스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
- - 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 즉시 전환됩니다.
+2. **QR 스캔 검증 실패 (팝업 유지 및 연속 재스캔 지원)**
+ - 스캔된 QR 코드가 불일치(대조 실패)할 경우, 팝업 창이 **자동으로 닫히지 않고 열린 상태를 유지**하며 "QR 바코드 불일치" 경고 상태를 표출합니다.
+ - 팝업이 열려 있는 동안 바코드/QR 스캐너 또는 키보드 입력을 통해 올바른 QR 코드로 **연속 재스캔**할 수 있으며, 재스캔에서 대조 성공 시 자동으로 통과(닫힘) 및 DB `OK` 저장 처리됩니다.
+ - 대조 불일치 상태에서 작업자가 **[확인]** 또는 **[취소]** (ESC 키) 버튼을 누르면 팝업 창이 닫히며, 판정 결과 **`NG`**로 데이터베이스 저장을 완료합니다.
+ - 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 전환됩니다.
- 공정 진행 상태 및 시스템 상태 바에 `"마킹 완료 / 각인 QR 검증 NG"`, `"상태: 검사 불합격 (각인 QR 검증 실패)"`, `"각인 QR 검증 실패 (DB NG 저장 완료)"` 등 구체적인 원인 설명이 표시되며, DB에 최종 `NG` 결과로 기록됩니다.
diff --git a/marking_gui/Manual/Manual_Merged.md b/marking_gui/Manual/Manual_Merged.md
index b776e98..dbf36f9 100644
--- a/marking_gui/Manual/Manual_Merged.md
+++ b/marking_gui/Manual/Manual_Merged.md
@@ -658,7 +658,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
* **세부 정보 그리드**:
- **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- **일련번호 정보**: `IC S/N`, **`Marking QR :`** (마킹 생성 QR 값), **`Scan QR :`** (스캐너 실측 QR 값), `각인 번호` 4개 항목을 수직 배열로 표출
- - **최종 판정결과**: 최종 **PASS/OK** (초록색) / **FAIL/NG** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **NG**로 자동 반영됨)
+ - **최종 판정결과**: 최종 **`OK`** (초록색) / **`NG`** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **`NG`**로 자동 반영됨)
- **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
---
@@ -840,8 +840,8 @@ graph TD
H --> I[12~14단계: 전압 / 저항 / 전류 실측]
I --> J[15단계: 테스트 주전원 안전 차단]
J --> K{최종 규격 비교 판정}
- K -- 1개 이상 이탈 FAIL --> L[대형 FAIL 표시 & 마킹 제외 & DB 불합격 적재]
- K -- 모두 통과 PASS --> M[대형 PASS 표시 & DB 합격 적재 & LMC1 레이저 QR 각인]
+ K -- 1개 이상 이탈 NG --> L[대형 NG 표시 & 마킹 제외 & DB NG 적재]
+ K -- 모두 통과 OK --> M[대형 OK 표시 & DB OK 적재 & LMC1 레이저 QR 각인]
L --> Z
M --> Z
```
@@ -880,13 +880,13 @@ graph TD
계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.
-1. **기준 규격을 벗어난 경우 (FAIL)**:
- * 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다.
- * 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
-2. **모든 항목이 합격인 경우 (PASS)**:
- * 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
- * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
- * 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
+1. **기준 규격을 벗어난 경우 (NG)**:
+ * 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(**`NG`**) 판정이 내려집니다.
+ * 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, `NG` 내역으로 DB에 기록됩니다.
+2. **모든 항목이 합격인 경우 (OK)**:
+ * 모든 수치가 기준치 내에 들어오면 최종 합격(**`OK`**) 판정이 내려집니다.
+ * 합격 판정과 동시에 선택된 EZD 도안 파일(.ezd)에서 도면품번 및 HKMC품번을 실시간으로 추출하여 각인 및 QR 코드가 자동으로 조합되어 레이저 마킹이 실행됩니다.
+ * 마킹 완료 후 EZD 파싱 품번 기반의 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증하고, 검증 완료 시 해당 품번 데이터로 DB 저장을 완료합니다.
---
@@ -894,12 +894,14 @@ graph TD
레이저 마킹이 완료되면 검증을 위해 **QR 검증 팝업 창**이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
-1. **QR 스캔 검증 성공 (즉시 자동 닫힘)**
+1. **QR 스캔 검증 성공 (즉시 자동 닫힘 및 DB OK 적재)**
- 스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 **즉시 자동으로 닫히며** DB에 `OK` 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
-2. **QR 스캔 검증 실패 (NG 판정 및 설명 표출)**
- - 스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
- - 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 즉시 전환됩니다.
+2. **QR 스캔 검증 실패 (팝업 유지 및 연속 재스캔 지원)**
+ - 스캔된 QR 코드가 불일치(대조 실패)할 경우, 팝업 창이 **자동으로 닫히지 않고 열린 상태를 유지**하며 "QR 바코드 불일치" 경고 상태를 표출합니다.
+ - 팝업이 열려 있는 동안 바코드/QR 스캐너 또는 키보드 입력을 통해 올바른 QR 코드로 **연속 재스캔**할 수 있으며, 재스캔에서 대조 성공 시 자동으로 통과(닫힘) 및 DB `OK` 저장 처리됩니다.
+ - 대조 불일치 상태에서 작업자가 **[확인]** 또는 **[취소]** (ESC 키) 버튼을 누르면 팝업 창이 닫히며, 판정 결과 **`NG`**로 데이터베이스 저장을 완료합니다.
+ - 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 전환됩니다.
- 공정 진행 상태 및 시스템 상태 바에 `"마킹 완료 / 각인 QR 검증 NG"`, `"상태: 검사 불합격 (각인 QR 검증 실패)"`, `"각인 QR 검증 실패 (DB NG 저장 완료)"` 등 구체적인 원인 설명이 표시되며, DB에 최종 `NG` 결과로 기록됩니다.
diff --git a/marking_gui/Manual/Manual_for_pdf.html b/marking_gui/Manual/Manual_for_pdf.html
index e40fbdf..5a9e004 100644
--- a/marking_gui/Manual/Manual_for_pdf.html
+++ b/marking_gui/Manual/Manual_for_pdf.html
@@ -405,9 +405,9 @@ a:hover {
프로그램 실행 및 로그인
바탕화면의 Marking System (marking_gui) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.)
통신 상태 확인 및 채널 설정
-
우측 상단의 통신 설정 메뉴에서 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.
+
화면 하단 바의 포트 설정 버튼을 클릭하여 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.
검사 규격값 검증
-
우측 상단의 시험 설정 메뉴를 열고, 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.
+
화면 하단 바의 파라미터 설정 버튼을 클릭하여 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.
성능 검사 및 마킹 시작
안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 검사 시작 (START) 버튼을 눌러 계측 및 마킹을 수행합니다.
@@ -640,7 +640,7 @@ a:hover {
1) Setting.ini (주요 변경 가능 설정)
현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다.
[Settings]
-EzdFolderPath=c:\EzCad\EasyCfg ; EZD 도안 파일 기본 탐색/저장 폴더 경로
+EzdFolderPath=c:\EzCad ; EZD 도안 파일 기본 탐색/저장 폴더 경로
test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false)
[Equipment]
@@ -738,9 +738,9 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
3.2.3 [카드 3] 측정 데이터 수치 카드 (우측 상단)
실제 계측기를 통해 수집된 3대 전기 파라미터 수치와 설정된 규격 범위를 표시하는 3열 구조의 카드입니다.
-* 측정 전압 (Voltage): DMM 계측 장비가 측정한 실측 전압 수치 (V 단위, 기본 기준범위: 0.0V ~ 0.5V)
-* 측정 전류 (Current): Power Supply 전원공급 장치가 측정한 소모 전류 수치 (mA 단위, 기본 기준범위: 8.00mA ~ 10.00mA)
-* 측정 저항 (Resistance): DMM 측정 장비가 측정한 전기적 저항 수치 (kΩ 단위, 기본 기준범위: 0.400kΩ ~ 0.800kΩ)
+* 측정 전압 (Voltage): DMM 계측 장비가 측정한 실측 전압 수치 (V 단위, 기본 기준범위: 1.00V ~ 6.00V)
+* 측정 전류 (Current): Power Supply 전원공급 장치가 측정한 소모 전류 수치 (mA 단위, 기본 기준범위: 5.00mA ~ 10.00mA)
+* 측정 저항 (Resistance): DMM 측정 장비가 측정한 전기적 저항 수치 (kΩ 단위, 기본 기준범위: 8.000kΩ ~ 13.000kΩ)
* 알림: 각 수치 카드 하단에는 설정 파일(Setting.ini)에서 지정한 임계 규격 범위가 표시되며, 실측값이 범위 내에 들어오면 카드 테두리가 초록색(PASS)으로, 범위를 벗어나면 빨간색(FAIL)으로 자동 전환됩니다.
3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)
@@ -749,7 +749,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
* 세부 정보 그리드:
- 검사 시간 정보: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- 일련번호 정보: IC S/N, Marking QR : (마킹 생성 QR 값), Scan QR : (스캐너 실측 QR 값), 각인 번호 4개 항목을 수직 배열로 표출
- - 최종 판정결과: 최종 PASS/OK (초록색) / FAIL/NG (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 NG로 자동 반영됨)
+ - 최종 판정결과: 최종 OK (초록색) / NG (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 NG로 자동 반영됨)
- 시스템 상태: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
@@ -761,7 +761,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
포트 설정: 센서 통신 보드의 시리얼 COM 포트 및 채널 설정 창을 호출합니다.
강제 검사 (스킵): 이전 공정 이력이 없어도 검증을 건너뛰고 계측 및 마킹을 수행합니다.
테스트 마킹: 계측 과정 없이 레이저 각인 장비 동작을 테스트합니다.
-
참고: 강제 검사 (스킵) 및 테스트 마킹 버튼은 설정 파일(Setting.ini)에서 TestMode=true로 활성화된 경우에만 화면에 노출됩니다.
+
참고: 강제 검사 (스킵) 버튼은 설정 파일(Setting.ini)에서 test=true로 활성화된 경우에만 화면에 노출됩니다.
@@ -924,8 +924,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
H --> I[12~14단계: 전압 / 저항 / 전류 실측]
I --> J[15단계: 테스트 주전원 안전 차단]
J --> K{최종 규격 비교 판정}
- K -- 1개 이상 이탈 FAIL --> L[대형 FAIL 표시 & 마킹 제외 & DB 불합격 적재]
- K -- 모두 통과 PASS --> M[대형 PASS 표시 & DB 합격 적재 & LMC1 레이저 QR 각인]
+ K -- 1개 이상 이탈 NG --> L[대형 NG 표시 & 마킹 제외 & DB NG 적재]
+ K -- 모두 통과 OK --> M[대형 OK 표시 & DB OK 적재 & LMC1 레이저 QR 각인]
L --> Z
M --> Z
@@ -961,15 +961,15 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
7.3 최종 판정 및 마킹 실행 조건
계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.
-
기준 규격을 벗어난 경우 (FAIL):
-
전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다.
-
이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
+
기준 규격을 벗어난 경우 (NG):
+
전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(NG) 판정이 내려집니다.
+
이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, NG 내역으로 DB에 기록됩니다.
-
모든 항목이 합격인 경우 (PASS):
-
모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
-
합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
-
마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
+
모든 항목이 합격인 경우 (OK):
+
모든 수치가 기준치 내에 들어오면 최종 합격(OK) 판정이 내려집니다.
+
합격 판정과 동시에 선택된 EZD 도안 파일(.ezd)에서 도면품번 및 HKMC품번을 실시간으로 추출하여 각인 및 QR 코드가 자동으로 조합되어 레이저 마킹이 실행됩니다.
+
마킹 완료 후 EZD 파싱 품번 기반의 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증하고, 검증 완료 시 해당 품번 데이터로 DB 저장을 완료합니다.
@@ -977,15 +977,17 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
7.4 레이저 각인 QR 검증 팝업 및 결과 연동
레이저 마킹이 완료되면 검증을 위해 QR 검증 팝업 창이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
-
QR 스캔 검증 성공 (즉시 자동 닫힘)
+
QR 스캔 검증 성공 (즉시 자동 닫힘 및 DB OK 적재)
스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 즉시 자동으로 닫히며 DB에 OK 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
-
QR 스캔 검증 실패 (NG 판정 및 설명 표출)
+
QR 스캔 검증 실패 (팝업 유지 및 연속 재스캔 지원)
-
스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
-
팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 OK에서 NG (빨간색 테두리 및 배경)로 즉시 전환됩니다.
+
스캔된 QR 코드가 불일치(대조 실패)할 경우, 팝업 창이 자동으로 닫히지 않고 열린 상태를 유지하며 "QR 바코드 불일치" 경고 상태를 표출합니다.
+
팝업이 열려 있는 동안 바코드/QR 스캐너 또는 키보드 입력을 통해 올바른 QR 코드로 연속 재스캔할 수 있으며, 재스캔에서 대조 성공 시 자동으로 통과(닫힘) 및 DB OK 저장 처리됩니다.
+
대조 불일치 상태에서 작업자가 [확인] 또는 [취소] (ESC 키) 버튼을 누르면 팝업 창이 닫히며, 판정 결과 NG로 데이터베이스 저장을 완료합니다.
+
팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 OK에서 NG (빨간색 테두리 및 배경)로 전환됩니다.
공정 진행 상태 및 시스템 상태 바에 "마킹 완료 / 각인 QR 검증 NG", "상태: 검사 불합격 (각인 QR 검증 실패)", "각인 QR 검증 실패 (DB NG 저장 완료)" 등 구체적인 원인 설명이 표시되며, DB에 최종 NG 결과로 기록됩니다.
@@ -996,7 +998,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
본 제어 프로그램은 설비 가동 중에 발생하는 계측기와의 통신 상태, 측정값 정보 및 각종 예외 오류 상황을 로그 파일로 기록합니다.
8.1.1 로그 파일 보관 방법
-
보관 폴더: 프로그램 실행 파일 경로의 하위 Logs 폴더에 저장됩니다.
+
보관 폴더: 프로그램 설치 경로(C:\marking\) 하위 logs/ 폴더에 저장됩니다. (예: C:\marking\logs\2026-08-05.log)
파일명 규칙: 날짜별로 파일이 자동으로 생성되어 보관됩니다.
로그 중요도 분류:
정보 (INFO): 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록.
@@ -1070,7 +1072,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
현상: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함.
원인 및 해결:
-
도면 파일 점검: 시험 설정 화면에서 세팅해 놓은 작업 도면 파일이 실제 해당 경로에 존재하는지 점검하십시오.
+
도면 파일 점검: 메인 화면의 파일 선택 버튼을 통해 선택한 작업 도면 파일(.ezd)이 실제 해당 경로에 존재하는지 점검하십시오.
마킹기 전원/연결 점검: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오.
@@ -1092,8 +1094,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
현상: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.
원인 및 해결:
-
네트워크/DB 서버 점검: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. Database.ini 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
-
계정 정보 확인: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 LoginOptions.json 계정 목록에 올바르게 등록되어 있는지 재확인하십시오.
+
네트워크/DB 서버 점검: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(3초)에 의해 경고가 표출됩니다. Database.ini 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
+
계정 정보 확인: 입력한 사용자 ID 및 비밀번호가 MSSQL Users 테이블에 활성 상태(IsActive=1)로 등록되어 있는지 확인하십시오. (DB 접속 실패 시에도 LoginOptions.json 파일의 loginAccounts 배열에 등록된 계정으로 1차 인증을 진행할 수 있으므로, 필요시 해당 파일의 계정 설정도 함께 점검하십시오.)
diff --git a/marking_gui/Models/MarkingResult.cs b/marking_gui/Models/MarkingResult.cs
index 9734d8f..79f898c 100644
--- a/marking_gui/Models/MarkingResult.cs
+++ b/marking_gui/Models/MarkingResult.cs
@@ -24,12 +24,30 @@ namespace marking_gui.Models
///
public DateTime MarkedAt { get; set; }
+ ///
+ /// EZD 도안에서 파싱하여 사용된 도면품번
+ ///
+ public string DrawingNo { get; set; } = string.Empty;
+
+ ///
+ /// EZD 도안에서 파싱하여 사용된 HKMC품번
+ ///
+ public string HkmcNo { get; set; } = string.Empty;
+
+ ///
+ /// EZD 도안 품번 기반으로 최종 조합된 마킹/검증용 QR 코드 문자열
+ ///
+ public string QrString { get; set; } = string.Empty;
+
public MarkingResult()
{
Success = false;
ErrorCode = -1;
ErrorMessage = "Uninitialized";
MarkedAt = DateTime.MinValue;
+ DrawingNo = string.Empty;
+ HkmcNo = string.Empty;
+ QrString = string.Empty;
}
}
}
diff --git a/marking_gui/PortSettingsWindow.xaml b/marking_gui/PortSettingsWindow.xaml
index 82faaba..cac402c 100644
--- a/marking_gui/PortSettingsWindow.xaml
+++ b/marking_gui/PortSettingsWindow.xaml
@@ -105,7 +105,6 @@
-
diff --git a/marking_gui/PortSettingsWindow.xaml.cs b/marking_gui/PortSettingsWindow.xaml.cs
index a0cf7ff..230ee38 100644
--- a/marking_gui/PortSettingsWindow.xaml.cs
+++ b/marking_gui/PortSettingsWindow.xaml.cs
@@ -198,56 +198,6 @@ namespace marking_gui
}
}
- private void btnScanTest_Click(object sender, RoutedEventArgs e)
- {
- string scannerPort = cbScannerPort?.SelectedItem?.ToString() ?? "Auto";
- int scannerBaud = 115200;
- if (cbScannerBaudRate?.SelectedItem is ComboBoxItem selectedBaudItem && int.TryParse(selectedBaudItem.Content?.ToString(), out int parsedBaud))
- {
- scannerBaud = parsedBaud;
- }
-
- var tempSettings = new BarcodeScannerSettings
- {
- Enabled = true,
- PortName = scannerPort,
- BaudRate = scannerBaud
- };
-
- SerialBarcodeScanner tempScanner = null;
- try
- {
- try
- {
- tempScanner = new SerialBarcodeScanner(tempSettings);
- }
- catch { }
-
- string testSampleQr = "TEST_PORT_SCAN;DRAWING_01;HKMC_99;2607299999;";
- var qrTestWin = new QrVerificationWindow(testSampleQr, tempScanner)
- {
- Owner = this,
- Title = $"📷 [스캐너 포트 {scannerPort} / {scannerBaud} Baud] 연동 테스트"
- };
-
- bool? testResult = qrTestWin.ShowDialog();
-
- if (testResult == true)
- {
- MessageBox.Show($"스캐너 (포트: {scannerPort}) 통신 및 QR 대조 검증 성공!\n\n현재 선택한 포트로 스캐너가 정상 작동합니다.", "스캔 테스트 성공", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- else
- {
- MessageBox.Show($"스캔 테스트가 중단되었거나 취소되었습니다.", "스캔 테스트 종료", MessageBoxButton.OK, MessageBoxImage.Information);
- }
- }
- finally
- {
- try { tempScanner?.Stop(); } catch { }
- try { tempScanner?.Dispose(); } catch { }
- }
- }
-
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
diff --git a/marking_gui/QrVerificationWindow.xaml.cs b/marking_gui/QrVerificationWindow.xaml.cs
index 90cf727..0fb22e3 100644
--- a/marking_gui/QrVerificationWindow.xaml.cs
+++ b/marking_gui/QrVerificationWindow.xaml.cs
@@ -206,6 +206,14 @@ namespace marking_gui
txtStatusMessage.Text = "QR 코드 검증 성공";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green
}
+
+ // 합격 시 즉시 팝업 자동 닫기 (요청 사항: 합격은 자동 닫힘 유지)
+ if (!_isClosing)
+ {
+ _isClosing = true;
+ DialogResult = true;
+ Close();
+ }
}
else
{
@@ -238,14 +246,8 @@ namespace marking_gui
txtStatusMessage.Text = "QR 바코드 불일치";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
}
- }
- // 합격이든 불합격이든 스캔 완료 즉시 팝업 닫기
- if (!_isClosing)
- {
- _isClosing = true;
- DialogResult = _isPassed;
- Close();
+ // 불합격(대조 실패) 시 팝업을 자동으로 닫지 않고 유지하여 계속 스캔 데이터 수신
}
}
diff --git a/marking_gui/Services/Board4251Service.cs b/marking_gui/Services/Board4251Service.cs
index 68196ba..ea0e97c 100644
--- a/marking_gui/Services/Board4251Service.cs
+++ b/marking_gui/Services/Board4251Service.cs
@@ -209,6 +209,11 @@ namespace marking_gui.Services
string currentData;
lock (_bufferLock)
{
+ // 수신 버퍼 8KB(8192자) 상한선 제한하여 메모리 누수 및 오버플로우 방지
+ if (_receiveBuffer.Length > 8192)
+ {
+ _receiveBuffer.Clear();
+ }
_receiveBuffer.Append(data);
currentData = _receiveBuffer.ToString();
}
@@ -235,6 +240,14 @@ namespace marking_gui.Services
public void Dispose()
{
+ try
+ {
+ if (_communication != null)
+ {
+ _communication.DataReceived -= OnDataReceived;
+ }
+ }
+ catch { }
Disconnect();
_reconnectTimer?.Dispose();
_lock.Dispose();
diff --git a/marking_gui/Services/EzdMarkingService.cs b/marking_gui/Services/EzdMarkingService.cs
index b2c7d6a..81cf7d5 100644
--- a/marking_gui/Services/EzdMarkingService.cs
+++ b/marking_gui/Services/EzdMarkingService.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
+using System.Threading.Tasks;
using marking_gui.Models;
namespace marking_gui.Services
@@ -38,15 +39,9 @@ namespace marking_gui.Services
[DllImport("MarkEzdWrapper.dll", EntryPoint = "ChangeTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern int ChangeTextByNameW(IntPtr hHandle, string strTextName, string strTextNew);
- [DllImport("MarkEzdWrapper.dll", EntryPoint = "ChangeTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
- private static extern int ChangeTextByNameA(IntPtr hHandle, string strTextName, string strTextNew);
-
[DllImport("MarkEzdWrapper.dll", EntryPoint = "GetTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
private static extern int GetTextByNameW(IntPtr hHandle, string strTextName, System.Text.StringBuilder strText);
- [DllImport("MarkEzdWrapper.dll", EntryPoint = "GetTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
- private static extern int GetTextByNameA(IntPtr hHandle, string strTextName, System.Text.StringBuilder strText);
-
[DllImport("MarkEzdWrapper.dll", EntryPoint = "Mark", CallingConvention = CallingConvention.Cdecl)]
private static extern int Mark(IntPtr hHandle);
@@ -61,6 +56,7 @@ namespace marking_gui.Services
private IntPtr _lastWindowHandle = IntPtr.Zero;
private string _lastConfigPath = string.Empty;
+ private string _lastLoadedEzdPath = string.Empty;
public bool Initialize(string configPath, IntPtr windowHandle)
{
@@ -68,92 +64,74 @@ namespace marking_gui.Services
{
try
{
- _lastConfigPath = configPath;
- if (windowHandle != IntPtr.Zero) _lastWindowHandle = windowHandle;
+ _lastConfigPath = configPath;
+ if (windowHandle != IntPtr.Zero) _lastWindowHandle = windowHandle;
- _lastInitErrorMessage = string.Empty;
- _config = new MarkingConfigService();
- _ezdFolderPath = _config.EzdFolderPath;
-
- if (string.IsNullOrEmpty(_ezdFolderPath))
- {
- _lastInitErrorMessage = "Setting.ini 내 EzdFolderPath 경로가 비어 있습니다.";
- _isInitialized = false;
- return false;
- }
+ _lastInitErrorMessage = string.Empty;
+ _config = new MarkingConfigService();
+ _ezdFolderPath = _config.EzdFolderPath;
- // EZCAD.CFG 파일 유무 검증 및 상위/하위 경로 자동 탐색
- string cfgFile = Path.Combine(_ezdFolderPath, "EZCAD.CFG");
- if (!File.Exists(cfgFile))
- {
- if (File.Exists(@"c:\EzCad\EZCAD.CFG"))
+ if (string.IsNullOrEmpty(_ezdFolderPath))
{
- _ezdFolderPath = @"c:\EzCad";
+ _lastInitErrorMessage = "Setting.ini 내 EzdFolderPath 경로가 비어 있습니다.";
+ _isInitialized = false;
+ return false;
}
- else if (File.Exists(@"c:\EzCad\EasyCfg\EZCAD.CFG"))
+
+ // EZCAD.CFG 파일 유무 검증 및 상위/하위 경로 자동 탐색
+ string cfgFile = Path.Combine(_ezdFolderPath, "EZCAD.CFG");
+ if (!File.Exists(cfgFile))
{
- _ezdFolderPath = @"c:\EzCad\EasyCfg";
+ if (File.Exists(@"c:\EzCad\EZCAD.CFG"))
+ {
+ _ezdFolderPath = @"c:\EzCad";
+ }
+ else if (File.Exists(@"c:\EzCad\EasyCfg\EZCAD.CFG"))
+ {
+ _ezdFolderPath = @"c:\EzCad\EasyCfg";
+ }
}
- }
-
- if (!Directory.Exists(_ezdFolderPath))
- {
- _lastInitErrorMessage = string.Format("EzCad 폴더가 존재하지 않습니다: {0} (Setting.ini 설정 확인 필요)", _ezdFolderPath);
- _isInitialized = false;
- return false;
- }
- // 기존 핸들이 열려 있다면 정리
- if (_markingHandle != IntPtr.Zero)
- {
- try { Close(_markingHandle); } catch { }
- _markingHandle = IntPtr.Zero;
- }
+ if (!Directory.Exists(_ezdFolderPath))
+ {
+ _lastInitErrorMessage = string.Format("마킹 라이브러리 폴더가 존재하지 않습니다: {0}", _ezdFolderPath);
+ _isInitialized = false;
+ return false;
+ }
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 LMC 초기화 시도. EzCadPath: " + _ezdFolderPath + ", WindowHandle: " + windowHandle);
-
- // 1. InitMarkEzd() 호출로 디바이스 전역 제어 핸들 획득
- _markingHandle = InitMarkEzd();
- System.Diagnostics.Debug.WriteLine(string.Format("[EzdMarking] InitMarkEzd() 완료. 생성된 핸들 포인터: 0x{0:X}", _markingHandle.ToInt64()));
+ if (_markingHandle == IntPtr.Zero)
+ {
+ _markingHandle = InitMarkEzd();
+ }
- if (_markingHandle == IntPtr.Zero)
- {
- _isInitialized = false;
- _lastInitErrorMessage = "InitMarkEzd() 실행 결과 생성된 핸들 포인터가 Zero(Null)입니다. 마킹 보드 및 드라이버 연결 상태를 점검하세요.";
- return false;
- }
+ if (_markingHandle == IntPtr.Zero)
+ {
+ _lastInitErrorMessage = "InitMarkEzd() 생성 실패 (IntPtr.Zero)";
+ _isInitialized = false;
+ return false;
+ }
- // 2. 획득된 핸들을 이용해 Init() 장치 매핑 수행
- int result = Init(_markingHandle, _ezdFolderPath, windowHandle);
-
- if (result == 0) // 0 이 성공 코드
- {
- _isInitialized = true;
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 실서버 LMC 컨트롤 보드 장치 연결 및 초기화 성공.");
- }
- else
- {
- _isInitialized = false;
- _lastInitErrorMessage = string.Format("LMC 컨트롤 카드 통신 실패 (Init 에러코드: {0})", result);
- System.Diagnostics.Debug.WriteLine("[EzdMarking] LMC 장치 연결 실패. 에러 코드: " + result);
- try { Close(_markingHandle); } catch { }
- _markingHandle = IntPtr.Zero;
+ int result = Init(_markingHandle, _ezdFolderPath, windowHandle);
+ if (result == 0)
+ {
+ _isInitialized = true;
+ _lastLoadedEzdPath = string.Empty;
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹기 연동 성공");
+ return true;
+ }
+ else
+ {
+ _lastInitErrorMessage = string.Format("Init() 실패 코드: {0} (경로: {1})", result, _ezdFolderPath);
+ try { Close(_markingHandle); } catch { }
+ _markingHandle = IntPtr.Zero;
+ _isInitialized = false;
+ return false;
+ }
}
-
- return _isInitialized;
- }
- catch (DllNotFoundException ex)
- {
- _isInitialized = false;
- _lastInitErrorMessage = "MarkEzdWrapper.dll 또는 하위 C++ DLL 로드 실패 (x86/32비트 아키텍처 타겟팅 컴파일 여부 확인 필요)";
- System.Diagnostics.Debug.WriteLine("[EzdMarking] MarkEzdWrapper.dll 또는 하위 DLL(Lmc1.dll 등)을 로드할 수 없습니다. 실행 폴더에 복사했는지 확인하세요. 상세: " + ex.Message);
- return false;
- }
catch (Exception ex)
{
+ _lastInitErrorMessage = "초기화 도중 예외: " + ex.Message;
_isInitialized = false;
- _lastInitErrorMessage = "마킹 컨트롤 보드 연동 예외 발생: " + ex.Message;
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 초기화 예외 발생: " + ex.Message);
return false;
}
}
@@ -163,167 +141,177 @@ namespace marking_gui.Services
[System.Security.SecurityCritical]
public MarkingResult RequestMarking(MarkingRequest request)
{
- if (!_isInitialized || _markingHandle == IntPtr.Zero)
- {
- // 각인 요청 시 미초기화 상태이면 1회 자동 재시도(Retry) 수행
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 디바이스 미초기화 상태 감지. 재연결 시도...");
- Initialize(_lastConfigPath, _lastWindowHandle);
- }
-
- if (!_isInitialized || _markingHandle == IntPtr.Zero)
- {
- return new MarkingResult
- {
- Success = false,
- ErrorCode = -10,
- ErrorMessage = "LMC 마킹 디바이스가 연결 및 초기화되지 않았습니다. 상세 원인: " + _lastInitErrorMessage,
- MarkedAt = DateTime.Now
- };
- }
-
- try
+ lock (_handleLock)
{
- // 1. 도안 파일 경로 확인
- string ezdFile = request.EzdFileName;
- string ezdPath = ezdFile;
-
- if (!File.Exists(ezdPath))
+ if (!_isInitialized || _markingHandle == IntPtr.Zero)
{
- ezdPath = Path.Combine(_ezdFolderPath, ezdFile);
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 디바이스 미초기화 상태 감지. 재연결 시도...");
+ Initialize(_lastConfigPath, _lastWindowHandle);
}
- if (!File.Exists(ezdPath))
+ if (!_isInitialized || _markingHandle == IntPtr.Zero)
{
- ezdPath = Path.Combine(_ezdFolderPath, "EasyCfg", ezdFile);
- }
-
- if (!File.Exists(ezdPath))
- {
- // 최종 확인 실패 시 에러
return new MarkingResult
{
Success = false,
- ErrorCode = -11,
- ErrorMessage = string.Format("도안 파일(.ezd)이 존재하지 않습니다: {0}", ezdFile),
+ ErrorCode = -10,
+ ErrorMessage = "LMC 마킹 디바이스가 연결 및 초기화되지 않았습니다. 상세 원인: " + _lastInitErrorMessage,
MarkedAt = DateTime.Now
};
}
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 도안 파일 로드 시작: " + ezdPath);
- string absoluteEzdPath = Path.GetFullPath(ezdPath);
- int loadResult = LoadEzdFile(_markingHandle, absoluteEzdPath); // 획득한 핸들 전달
- if (loadResult != 0)
+ try
{
- return new MarkingResult
+ string ezdFile = request.EzdFileName;
+ string ezdPath = ezdFile;
+
+ if (!File.Exists(ezdPath)) ezdPath = Path.Combine(_ezdFolderPath, ezdFile);
+ if (!File.Exists(ezdPath)) ezdPath = Path.Combine(_ezdFolderPath, "EasyCfg", ezdFile);
+
+ if (!File.Exists(ezdPath))
{
- Success = false,
- ErrorCode = loadResult,
- ErrorMessage = string.Format("도안 파일(.ezd) 로드 실패. (경로: {0}, LoadEzdFile 에러코드: {1})", absoluteEzdPath, loadResult),
- MarkedAt = DateTime.Now
- };
- }
+ return new MarkingResult
+ {
+ Success = false,
+ ErrorCode = -11,
+ ErrorMessage = string.Format("도안 파일(.ezd)이 존재하지 않습니다: {0}", ezdFile),
+ MarkedAt = DateTime.Now
+ };
+ }
- // 1-1. 도안 파일(.ezd) 내부의 고정 텍스트 오브젝트(도면품번, HKMC 품번)를 직접 읽어오기
- string ezdDrawingNo = GetTextFromEzd(_config.DrawingNoTag);
- string ezdHkmcNo = GetTextFromEzd(_config.HkmcNoTag);
+ string absoluteEzdPath = Path.GetFullPath(ezdPath);
- if (!string.IsNullOrWhiteSpace(ezdDrawingNo))
- {
- request.DrawingNo = ezdDrawingNo;
- }
- if (!string.IsNullOrWhiteSpace(ezdHkmcNo))
- {
- request.HkmcNo = ezdHkmcNo;
- }
+ if (!string.Equals(_lastLoadedEzdPath, absoluteEzdPath, StringComparison.OrdinalIgnoreCase))
+ {
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 도안 파일 신규 로드: " + absoluteEzdPath);
+ int loadResult = LoadEzdFile(_markingHandle, absoluteEzdPath);
+ if (loadResult != 0)
+ {
+ _lastLoadedEzdPath = string.Empty;
+ return new MarkingResult
+ {
+ Success = false,
+ ErrorCode = loadResult,
+ ErrorMessage = string.Format("도안 파일(.ezd) 로드 실패. (경로: {0}, LoadEzdFile 에러코드: {1})", absoluteEzdPath, loadResult),
+ MarkedAt = DateTime.Now
+ };
+ }
+ _lastLoadedEzdPath = absoluteEzdPath;
+ }
- // EZD 고정값 기반으로 QR 코드 데이터 문자열 최종 재조합 ([IC S/N];[도면품번];[HKMC품번];[제품 S/N];)
- string icSerial = string.IsNullOrWhiteSpace(request.IcSerial) ? request.Serial : request.IcSerial;
- request.QrString = string.Format("{0};{1};{2};{3};", icSerial, request.DrawingNo, request.HkmcNo, request.Serial);
+ string ezdDrawingNo = GetTextFromEzd(_config.DrawingNoTag, absoluteEzdPath);
+ string ezdHkmcNo = GetTextFromEzd(_config.HkmcNoTag, absoluteEzdPath);
- // 2. 도안 내 지정된 5개 변수 태그만 치환 (SERIAL, YYMMDD, DATAMATRIX, PARTNO, HKMCPO)
- string seqOnly = (!string.IsNullOrEmpty(request.Serial) && request.Serial.Length >= 4)
- ? request.Serial.Substring(request.Serial.Length - 4)
- : request.Serial;
+ if (string.IsNullOrWhiteSpace(ezdDrawingNo) && _config != null)
+ ezdDrawingNo = _config.GetDrawingNoForEzd(absoluteEzdPath);
+ if (string.IsNullOrWhiteSpace(ezdHkmcNo) && _config != null)
+ ezdHkmcNo = _config.GetHkmcNoForEzd(absoluteEzdPath);
- ChangeTextInEzd(_config.SerialTag, seqOnly); // 1. SERIAL (4자리 순번)
- ChangeTextInEzd(_config.LotNoTag, request.LotNo); // 2. YYMMDD (LotNo)
- ChangeTextInEzd(_config.QrCodeTag, request.QrString); // 3. DATAMATRIX (QR코드)
- ChangeTextInEzd(_config.DrawingNoTag, request.DrawingNo); // 4. PARTNO (도면품번)
- ChangeTextInEzd(_config.HkmcNoTag, request.HkmcNo); // 5. HKMCPO (HKMC품번)
+ if (!string.IsNullOrWhiteSpace(ezdDrawingNo)) request.DrawingNo = ezdDrawingNo;
+ if (!string.IsNullOrWhiteSpace(ezdHkmcNo)) request.HkmcNo = ezdHkmcNo;
- System.Diagnostics.Debug.WriteLine(string.Format(
- "[EzdMarking] 도안 텍스트 치환 완료. Serial:{0}, LotNo:{1}, Product:{2}, Drawing:{3}, Hkmc:{4}, Qr:{5}",
- request.Serial, request.LotNo, request.ProductNo, request.DrawingNo, request.HkmcNo, request.QrString));
+ string icSerial = string.IsNullOrWhiteSpace(request.IcSerial) ? request.Serial : request.IcSerial;
+ request.QrString = string.Format("{0};{1};{2};{3};", icSerial, request.DrawingNo, request.HkmcNo, request.Serial);
- // 3. 레이저 마킹 시작 (획득한 핸들 전달)
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 레이저 마킹 기계 동작 시작...");
- int markResult = Mark(_markingHandle);
+ string seqOnly = (!string.IsNullOrEmpty(request.Serial) && request.Serial.Length >= 4)
+ ? request.Serial.Substring(request.Serial.Length - 4)
+ : request.Serial;
- if (markResult == 0)
- {
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 동작 완료 (성공)");
- return new MarkingResult
+ ChangeTextInEzd(_config.SerialTag, seqOnly);
+ ChangeTextInEzd(_config.LotNoTag, request.LotNo);
+ ChangeTextInEzd(_config.QrCodeTag, request.QrString);
+ if (!string.IsNullOrWhiteSpace(request.DrawingNo)) ChangeTextInEzd(_config.DrawingNoTag, request.DrawingNo);
+ if (!string.IsNullOrWhiteSpace(request.HkmcNo)) ChangeTextInEzd(_config.HkmcNoTag, request.HkmcNo);
+
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 레이저 마킹 기계 동작 시작 (30초 타임아웃 제한)...");
+ IntPtr handleToMark = _markingHandle;
+ var markingTask = Task.Run(() => Mark(handleToMark));
+ var timeoutTask = Task.Delay(30000);
+
+ var completedTask = Task.WhenAny(markingTask, timeoutTask).Result;
+
+ if (completedTask == timeoutTask)
{
- Success = true,
- ErrorCode = 0,
- ErrorMessage = "마킹 완료",
- MarkedAt = DateTime.Now
- };
- }
- else
- {
- System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 동작 에러 발생: " + markResult);
- return new MarkingResult
+ _isInitialized = false;
+ _markingHandle = IntPtr.Zero;
+ _lastLoadedEzdPath = string.Empty;
+ return new MarkingResult { Success = false, ErrorCode = -98, ErrorMessage = "레이저 각인 하드웨어 응답 타임아웃 (30초)", MarkedAt = DateTime.Now };
+ }
+
+ int markResult = markingTask.Result;
+ if (markResult == 0)
{
- Success = false,
- ErrorCode = markResult,
- ErrorMessage = string.Format("레이저 각인 하드웨어 동작 에러 (에러코드: {0})", markResult),
- MarkedAt = DateTime.Now
- };
+ return new MarkingResult
+ {
+ Success = true,
+ ErrorCode = 0,
+ ErrorMessage = "마킹 완료",
+ MarkedAt = DateTime.Now,
+ DrawingNo = request.DrawingNo,
+ HkmcNo = request.HkmcNo,
+ QrString = request.QrString
+ };
+ }
+ else
+ {
+ _isInitialized = false;
+ _markingHandle = IntPtr.Zero;
+ _lastLoadedEzdPath = string.Empty;
+ return new MarkingResult { Success = false, ErrorCode = markResult, ErrorMessage = string.Format("레이저 각인 하드웨어 동작 에러 (에러코드: {0})", markResult), MarkedAt = DateTime.Now };
+ }
}
- }
- catch (Exception ex)
- {
- System.Diagnostics.Debug.WriteLine("[EzdMarking] RequestMarking 실행 중 예외 발생: " + ex.Message);
- return new MarkingResult
+ catch (Exception ex)
{
- Success = false,
- ErrorCode = -99,
- ErrorMessage = "마킹 처리 중 예외 발생: " + ex.Message,
- MarkedAt = DateTime.Now
- };
+ _isInitialized = false;
+ _markingHandle = IntPtr.Zero;
+ _lastLoadedEzdPath = string.Empty;
+ return new MarkingResult { Success = false, ErrorCode = -99, ErrorMessage = "마킹 처리 중 예외 발생: " + ex.Message, MarkedAt = DateTime.Now };
+ }
}
}
private bool _disposed = false;
- private string GetTextFromEzd(string tagName)
+ public string GetTextFromEzd(string tagName, string ezdFilePath = null)
{
- if (string.IsNullOrWhiteSpace(tagName) || _markingHandle == IntPtr.Zero)
- return string.Empty;
+ if (string.IsNullOrWhiteSpace(tagName)) return string.Empty;
- try
+ // 1단계: P/Invoke GetTextByNameW 호출 (DLL 지원 시 도안 텍스트 읽기)
+ if (_markingHandle != IntPtr.Zero)
{
- // 1차: Unicode (UTF-16) 시도
- var sbW = new System.Text.StringBuilder(512);
- int resW = GetTextByNameW(_markingHandle, tagName, sbW);
- if (resW == 0 && sbW.Length > 0)
+ try
{
- return sbW.ToString().Trim();
+ var sbW = new System.Text.StringBuilder(512);
+ int resW = GetTextByNameW(_markingHandle, tagName, sbW);
+ if (resW == 0 && sbW.Length > 0)
+ {
+ string val = sbW.ToString().Trim();
+ if (!string.IsNullOrWhiteSpace(val)) return val;
+ }
}
-
- // 2차: Ansi (CP949/EUC-KR) 시도 (C++ Wrapper가 ANSI인 경우 대비)
- var sbA = new System.Text.StringBuilder(512);
- int resA = GetTextByNameA(_markingHandle, tagName, sbA);
- if (resA == 0 && sbA.Length > 0)
+ catch (EntryPointNotFoundException)
+ {
+ System.Diagnostics.Debug.WriteLine($"[EzdMarking] GetTextByName({tagName}) DLL 내 엔트리포인트 없음.");
+ }
+ catch (Exception ex)
{
- return sbA.ToString().Trim();
+ System.Diagnostics.Debug.WriteLine($"[EzdMarking] GetTextByName({tagName}) 읽기 예외: {ex.Message}");
}
}
- catch (Exception ex)
+
+ // 2단계: Setting.ini / MarkingConfigService 사용자 명시 설정값 참조
+ if (_config != null)
{
- System.Diagnostics.Debug.WriteLine($"[EzdMarking] GetTextByName({tagName}) 읽기 예외: {ex.Message}");
+ if (tagName.Equals(_config.DrawingNoTag, StringComparison.OrdinalIgnoreCase))
+ {
+ return _config.GetDrawingNoForEzd(ezdFilePath);
+ }
+ if (tagName.Equals(_config.HkmcNoTag, StringComparison.OrdinalIgnoreCase))
+ {
+ return _config.GetHkmcNoForEzd(ezdFilePath);
+ }
}
+
return string.Empty;
}
@@ -332,11 +320,7 @@ namespace marking_gui.Services
if (string.IsNullOrWhiteSpace(tagName) || _markingHandle == IntPtr.Zero) return;
try
{
- int resW = ChangeTextByNameW(_markingHandle, tagName, textNew);
- if (resW != 0)
- {
- ChangeTextByNameA(_markingHandle, tagName, textNew);
- }
+ ChangeTextByNameW(_markingHandle, tagName, textNew);
}
catch
{
diff --git a/marking_gui/Services/MarkingConfigService.cs b/marking_gui/Services/MarkingConfigService.cs
index e6a55fd..b18fd45 100644
--- a/marking_gui/Services/MarkingConfigService.cs
+++ b/marking_gui/Services/MarkingConfigService.cs
@@ -122,6 +122,30 @@ namespace marking_gui.Services
public string DrawingNo => GetSetting("DrawingNo", string.Empty);
public string HkmcNo => GetSetting("HkmcNo", string.Empty);
+ public string GetDrawingNoForEzd(string ezdFileName)
+ {
+ if (!string.IsNullOrWhiteSpace(ezdFileName))
+ {
+ string fileNameOnly = System.IO.Path.GetFileName(ezdFileName);
+ string key = "DrawingNo_" + fileNameOnly;
+ string val = GetSetting(key, string.Empty);
+ if (!string.IsNullOrWhiteSpace(val)) return val;
+ }
+ return DrawingNo;
+ }
+
+ public string GetHkmcNoForEzd(string ezdFileName)
+ {
+ if (!string.IsNullOrWhiteSpace(ezdFileName))
+ {
+ string fileNameOnly = System.IO.Path.GetFileName(ezdFileName);
+ string key = "HkmcNo_" + fileNameOnly;
+ string val = GetSetting(key, string.Empty);
+ if (!string.IsNullOrWhiteSpace(val)) return val;
+ }
+ return HkmcNo;
+ }
+
public bool IsTestMode
{
get
diff --git a/marking_gui/Services/SerialBarcodeScanner.cs b/marking_gui/Services/SerialBarcodeScanner.cs
index 3c51632..cd356ab 100644
--- a/marking_gui/Services/SerialBarcodeScanner.cs
+++ b/marking_gui/Services/SerialBarcodeScanner.cs
@@ -67,12 +67,23 @@ namespace marking_gui.Services
private readonly BarcodeScannerSettings _settings;
private readonly HashSet _excludedPortNames;
private readonly List _sessions = new List();
+ private readonly object _sessionsLock = new object();
+ private Timer? _autoReconnectTimer;
private string? _confirmedPortName;
private bool _disposed;
public event EventHandler? ScanReceived;
- public bool IsRunning => _sessions.Count > 0;
+ public bool IsRunning
+ {
+ get
+ {
+ lock (_sessionsLock)
+ {
+ return _sessions.Any(s => s.Port.IsOpen);
+ }
+ }
+ }
public string? ConfirmedPortName => _confirmedPortName;
public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable? excludedPortNames = null)
@@ -87,6 +98,17 @@ namespace marking_gui.Services
Stop();
+ lock (_sessionsLock)
+ {
+ OpenCandidatePorts();
+ }
+
+ // 5초 주기로 닫힌 포트 자동 재연결 모니터링 타이머 가동
+ _autoReconnectTimer ??= new Timer(_ => AutoReconnectCallback(), null, 5000, 5000);
+ }
+
+ private void OpenCandidatePorts()
+ {
var candidatePorts = GetCandidatePortNames();
if (candidatePorts.Length == 0)
{
@@ -96,6 +118,9 @@ namespace marking_gui.Services
foreach (var portName in candidatePorts)
{
+ if (_sessions.Any(s => string.Equals(s.Port.PortName, portName, StringComparison.OrdinalIgnoreCase)))
+ continue;
+
try
{
var port = CreateSerialPort(portName);
@@ -117,19 +142,59 @@ namespace marking_gui.Services
}
}
- public void Stop()
+ private void AutoReconnectCallback()
{
- foreach (var session in _sessions)
+ if (_disposed || !_settings.Enabled) return;
+
+ lock (_sessionsLock)
{
try
{
- session.Port.DataReceived -= SerialPort_DataReceived;
- session.Dispose();
+ // 닫히거나 훼손된 포트 세션 정리
+ for (int i = _sessions.Count - 1; i >= 0; i--)
+ {
+ var session = _sessions[i];
+ if (!session.Port.IsOpen)
+ {
+ try
+ {
+ session.Port.DataReceived -= SerialPort_DataReceived;
+ session.Dispose();
+ }
+ catch { }
+ _sessions.RemoveAt(i);
+ }
+ }
+
+ // 닫힌 포트가 발견되었을 때만 후보 포트 재오픈 시도
+ OpenCandidatePorts();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] AutoReconnect 예외: {ex.Message}");
}
- catch { }
}
- _sessions.Clear();
- _confirmedPortName = null;
+ }
+
+ public void Stop()
+ {
+ _autoReconnectTimer?.Dispose();
+ _autoReconnectTimer = null;
+
+ lock (_sessionsLock)
+ {
+ foreach (var session in _sessions)
+ {
+ try
+ {
+ session.Port.DataReceived -= SerialPort_DataReceived;
+ session.Dispose();
+ }
+ catch { }
+ }
+ _sessions.Clear();
+ _confirmedPortName = null;
+ }
}
public void ConfirmPort(string portName)
@@ -141,19 +206,22 @@ namespace marking_gui.Services
_confirmedPortName = normalized;
- // 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
- for (int i = _sessions.Count - 1; i >= 0; i--)
+ lock (_sessionsLock)
{
- var session = _sessions[i];
- if (!string.Equals(session.Port.PortName, normalized, StringComparison.OrdinalIgnoreCase))
+ // 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
+ for (int i = _sessions.Count - 1; i >= 0; i--)
{
- try
+ var session = _sessions[i];
+ if (!string.Equals(session.Port.PortName, normalized, StringComparison.OrdinalIgnoreCase))
{
- session.Port.DataReceived -= SerialPort_DataReceived;
- session.Dispose();
+ try
+ {
+ session.Port.DataReceived -= SerialPort_DataReceived;
+ session.Dispose();
+ }
+ catch { }
+ _sessions.RemoveAt(i);
}
- catch { }
- _sessions.RemoveAt(i);
}
}
}
@@ -212,7 +280,11 @@ namespace marking_gui.Services
{
if (sender is not SerialPort port) return;
- var session = _sessions.FirstOrDefault(s => ReferenceEquals(s.Port, port));
+ PortSession? session = null;
+ lock (_sessionsLock)
+ {
+ session = _sessions.FirstOrDefault(s => ReferenceEquals(s.Port, port));
+ }
if (session == null) return;
try
@@ -222,6 +294,11 @@ namespace marking_gui.Services
lock (session.Buffer)
{
+ // 수신 버퍼 4KB(4096자) 상한선 제한하여 메모리 누수 방지
+ if (session.Buffer.Length > 4096)
+ {
+ session.Buffer.Clear();
+ }
session.Buffer.Append(text);
}
@@ -248,7 +325,14 @@ namespace marking_gui.Services
bool isConfirmed = string.Equals(_confirmedPortName, session.Port.PortName, StringComparison.OrdinalIgnoreCase);
- ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed));
+ try
+ {
+ ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed));
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ScanReceived Event Error] {ex.Message}");
+ }
}
public void Dispose()
diff --git a/marking_gui/Setting.ini b/marking_gui/Setting.ini
index 6e81465..5312958 100644
--- a/marking_gui/Setting.ini
+++ b/marking_gui/Setting.ini
@@ -9,10 +9,8 @@ EzdFolderPath=c:\EzCad
; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출 반대 false)
test=true
-; 도면품번 (EZD 도안의 도면품번 태그에 채워질 값 — DrawingNoTag에 매핑)
+; 도면품번 및 HKMC품번 지정 (사용자가 설정)
DrawingNo=APTSL04A
-
-; HKMC 품번 (EZD 도안의 HKMC 품번 태그에 채워질 값 — HkmcNoTag에 매핑)
HkmcNo=254H1-QI080
@@ -36,6 +34,7 @@ DrawingNoTag=PARTNO
HkmcNoTag=HKMCPO
+
; =================================================================
; 계측기 연동 설정 (SCPI / LAN)
; =================================================================
diff --git a/marking_gui/SettingsWindow.xaml.cs b/marking_gui/SettingsWindow.xaml.cs
index 208a28b..1c3ab66 100644
--- a/marking_gui/SettingsWindow.xaml.cs
+++ b/marking_gui/SettingsWindow.xaml.cs
@@ -17,8 +17,8 @@ namespace marking_gui
Limits = new MeasurementLimits(limits.MinVoltage, limits.MaxVoltage, limits.MinCurrent, limits.MaxCurrent, limits.MinResistance, limits.MaxResistance);
- txtMinVoltage.Text = Limits.MinVoltage.ToString("F1");
- txtMaxVoltage.Text = Limits.MaxVoltage.ToString("F1");
+ txtMinVoltage.Text = Limits.MinVoltage.ToString("F2");
+ txtMaxVoltage.Text = Limits.MaxVoltage.ToString("F2");
txtMinCurrent.Text = Limits.MinCurrent.ToString("F2");
txtMaxCurrent.Text = Limits.MaxCurrent.ToString("F2");
txtMinResistance.Text = Limits.MinResistance.ToString("F3");
diff --git a/test_bukuk.ezd b/test_bukuk.ezd
new file mode 100644
index 0000000000000000000000000000000000000000..79258c99ee469f0e538859f444cb9f21f0ceede9
GIT binary patch
literal 445633
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