@@ -613,7 +429,7 @@ a:hover {
| **자동 계측 시퀀스** | 전원 공급 장치와 전압/저항 측정 장비들과 연동하여 제품에 테스트 전원을 투입하고 전압, 저항, 전류 값을 차례대로 계측합니다. |
| **실제 저항값 측정** | 전압/저항 DMM 측정기를 통해 센서의 실제 저항값(kΩ)을 측정합니다. |
| **전류 측정** | 전원 공급 장치(Power Supply)의 출력 채널 1을 통해 제품의 소모 전류(mA)를 계측합니다. |
-| **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 2D QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. |
+| **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. |
| **결과 기록** | 계측 판정 규격(전압, 전류, 저항)과 마킹 성공 여부를 최종 판정하여 생산 데이터베이스에 저장합니다. |
---
@@ -638,7 +454,7 @@ a:hover {
| **센서 판독 보드 (4251 보드)** | 제품 내부 칩의 일련번호(IC_SN)를 인식합니다. | RS-232 시리얼 통신 |
| **전압 및 저항 측정 장비 (DMM)** | 제품의 전압(V) 및 전기적 저항(kΩ)을 측정합니다. | TCP/IP 네트워크 통신 |
| **전류 측정 및 전원 공급 장비 (PSU)** | 제품 구동을 위한 전원 공급 및 소모 전류(mA)를 측정합니다. | TCP/IP 네트워크 통신 |
-| **레이저 마킹기** | 2D QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 |
+| **레이저 마킹기** | QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 |
## 1.4 설치 경로(`C:\marking\`) 구조 및 생성 파일 역할
@@ -823,7 +639,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
### 3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단)
생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다.
-* **1. Housing**: 하우징 조립 공정 검사 판정 상태 (`PASS` / `FAIL` / `대기`)
+* **1. Housing**: 하우징 조립 공정 검사 판정 상태 (`PASS` / `FAIL` / `대기`) 및 서브 텍스트로 **`PCB Barcode: [값]`**을 함께 표시합니다.
+ * *주의: Housing 검사가 PASS이더라도 PCB 바코드 값을 읽어오지 못하거나 누락된 경우 Housing 항목은 **FAIL**로 처리됩니다.*
* **2. Calibration**: 센서 압력 보정 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
* **3. EOL**: EOL 1차 성능 평가 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
* *주의: 이전 공정 이력 중 하나라도 `FAIL` 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)*
@@ -837,12 +654,12 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
### 3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)
진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다.
-* **💬 진행 상황 뱃지**: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 설비 시퀀스 동작을 실시간 텍스트로 표출합니다.
+* **💬 진행 상황 뱃지**: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중...", "각인 QR 검증 실패 (DB NG 저장 완료)" 등 설비 시퀀스 및 검증 동작을 실시간 텍스트로 표출합니다.
* **세부 정보 그리드**:
- **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- - **일련번호 정보**: IC S/N, PCB 바코드, 실제 각인 완료 번호 표출
- - **최종 판정결과**: 최종 **PASS** (초록색) / **FAIL** (빨간색) 대형 배지 표출
- - **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러 메시지 배너 표출
+ - **일련번호 정보**: `IC S/N`, **`Marking QR :`** (마킹 생성 QR 값), **`Scan QR :`** (스캐너 실측 QR 값), `각인 번호` 4개 항목을 수직 배열로 표출
+ - **최종 판정결과**: 최종 **PASS/OK** (초록색) / **FAIL/NG** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **NG**로 자동 반영됨)
+ - **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
---
@@ -886,6 +703,10 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
- 드롭다운 메뉴를 통해 **`1`** 또는 **`2`** 채널을 선택할 수 있습니다.
- 선택된 채널 및 포트 정보는 `Sensor.ini` 파일에 저장됩니다.
+4. **바코드 / QR 스캐너 포트 (Scanner Port)**:
+ - 각인 후 QR 검증 시 사용되는 바코드/QR 스캐너의 연결 포트를 지정합니다. (`Auto` 또는 특정 `COM` 포트 선택)
+ - `Auto` 선택 시 입력 수신 단자를 자동으로 자동 인식합니다.
+
---
## 4.2 설정 저장
@@ -1064,8 +885,22 @@ graph TD
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (PASS)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
- * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
- * 마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다.
+ * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
+ * 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
+
+---
+
+## 7.4 레이저 각인 QR 검증 팝업 및 결과 연동
+
+레이저 마킹이 완료되면 검증을 위해 **QR 검증 팝업 창**이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
+
+1. **QR 스캔 검증 성공 (즉시 자동 닫힘)**
+ - 스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 **즉시 자동으로 닫히며** DB에 `OK` 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
+
+2. **QR 스캔 검증 실패 (NG 판정 및 설명 표출)**
+ - 스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
+ - 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 즉시 전환됩니다.
+ - 공정 진행 상태 및 시스템 상태 바에 `"마킹 완료 / 각인 QR 검증 NG"`, `"상태: 검사 불합격 (각인 QR 검증 실패)"`, `"각인 QR 검증 실패 (DB NG 저장 완료)"` 등 구체적인 원인 설명이 표시되며, DB에 최종 `NG` 결과로 기록됩니다.
@@ -1155,6 +990,12 @@ graph TD
- **순번 범위 초과**: 현재 로트 번호(`Lot No`)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다.
- **해결 방법**: 새로운 생산 조 또는 로트 번호(`Lot No`)를 수정하여 **프로그램에 재로그인**하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다.
+### 9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오."
+* **현상**: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.
+* **원인 및 해결**:
+ 1. **네트워크/DB 서버 점검**: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. `Database.ini` 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
+ 2. **계정 정보 확인**: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 `LoginOptions.json` 계정 목록에 올바르게 등록되어 있는지 재확인하십시오.
+
---
## 9.2 프로그램 정상 종료 및 전원 자동 차단 규칙
diff --git a/marking_gui/Manual/Manual_for_pdf.html b/marking_gui/Manual/Manual_for_pdf.html
index 9a5a525..e40fbdf 100644
--- a/marking_gui/Manual/Manual_for_pdf.html
+++ b/marking_gui/Manual/Manual_for_pdf.html
@@ -291,7 +291,7 @@ a:hover {
.toc-list {
display: flex;
flex-direction: column;
- gap: 10px;
+ gap: 16px;
}
.toc-item {
@@ -299,24 +299,6 @@ a:hover {
align-items: baseline;
font-size: 11pt;
line-height: 1.5;
- margin-top: 6px;
-}
-
-.toc-sub-item {
- display: flex;
- align-items: baseline;
- font-size: 9.5pt;
- line-height: 1.4;
- padding-left: 18px;
-}
-
-.toc-sub2-item {
- display: flex;
- align-items: baseline;
- font-size: 9pt;
- line-height: 1.3;
- padding-left: 36px;
- color: #6b7280;
}
.toc-title {
@@ -326,29 +308,11 @@ a:hover {
padding-right: 6px;
}
-.toc-sub-item .toc-title {
- font-weight: 500;
- color: #374151;
-}
-
-.toc-sub2-item .toc-title {
- font-weight: 400;
- color: #4b5563;
-}
-
.toc-title a {
color: #1f2937 !important;
text-decoration: none !important;
}
-.toc-sub-item .toc-title a {
- color: #374151 !important;
-}
-
-.toc-sub2-item .toc-title a {
- color: #4b5563 !important;
-}
-
.toc-title a:hover {
color: #007acc !important;
}
@@ -361,14 +325,6 @@ a:hover {
top: -4px;
}
-.toc-sub-item .toc-leader {
- border-bottom: 1px dotted #cbd5e1;
-}
-
-.toc-sub2-item .toc-leader {
- border-bottom: 1px dotted #e2e8f0;
-}
-
.toc-page {
font-weight: 700;
color: #374151;
@@ -376,16 +332,6 @@ a:hover {
white-space: nowrap;
}
-.toc-sub-item .toc-page {
- font-weight: 600;
- color: #4b5563;
-}
-
-.toc-sub2-item .toc-page {
- font-weight: 400;
- color: #6b7280;
-}
-
@@ -404,176 +350,46 @@ a:hover {
3
-
성능 검사가 모두 통과된 합격품에 대해 제품 표면에 2D QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다.
+
성능 검사가 모두 통과된 합격품에 대해 제품 표면에 QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다.
결과 기록
@@ -712,7 +528,7 @@ a:hover {
레이저 마킹기
-
2D QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다.
+
QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다.
마킹 제어 보드 연동
@@ -914,7 +730,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단)
생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다.
-* 1. Housing: 하우징 조립 공정 검사 판정 상태 (PASS / FAIL / 대기)
+* 1. Housing: 하우징 조립 공정 검사 판정 상태 (PASS / FAIL / 대기) 및 서브 텍스트로 PCB Barcode: [값]을 함께 표시합니다.
+ * 주의: Housing 검사가 PASS이더라도 PCB 바코드 값을 읽어오지 못하거나 누락된 경우 Housing 항목은 FAIL로 처리됩니다.
* 2. Calibration: 센서 압력 보정 공정 판정 상태 (PASS / FAIL / 대기)
* 3. EOL: EOL 1차 성능 평가 공정 판정 상태 (PASS / FAIL / 대기)
* 주의: 이전 공정 이력 중 하나라도 FAIL 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)
@@ -928,12 +745,12 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)
진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다.
-* 💬 진행 상황 뱃지: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 설비 시퀀스 동작을 실시간 텍스트로 표출합니다.
+* 💬 진행 상황 뱃지: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중...", "각인 QR 검증 실패 (DB NG 저장 완료)" 등 설비 시퀀스 및 검증 동작을 실시간 텍스트로 표출합니다.
* 세부 정보 그리드:
- 검사 시간 정보: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- - 일련번호 정보: IC S/N, PCB 바코드, 실제 각인 완료 번호 표출
- - 최종 판정결과: 최종 PASS (초록색) / FAIL (빨간색) 대형 배지 표출
- - 시스템 상태: 통신 및 디바이스 가동 상태 또는 에러 메시지 배너 표출
+ - 일련번호 정보: IC S/N, Marking QR : (마킹 생성 QR 값), Scan QR : (스캐너 실측 QR 값), 각인 번호 4개 항목을 수직 배열로 표출
+ - 최종 판정결과: 최종 PASS/OK (초록색) / FAIL/NG (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 NG로 자동 반영됨)
+ - 시스템 상태: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
3.3 하단 제어 영역 (Control Box & Footer)
@@ -975,7 +792,14 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
센서 판독기 내부의 연동 채널 번호를 결정합니다.
드롭다운 메뉴를 통해 1 또는 2 채널을 선택할 수 있습니다.
-
선택된 채널 및 포트 정보는 Sensor.ini 파일에 저장됩니다.
+
+
선택된 채널 및 포트 정보는 Sensor.ini 파일에 저장됩니다.
+
+
+
바코드 / QR 스캐너 포트 (Scanner Port):
+
+
각인 후 QR 검증 시 사용되는 바코드/QR 스캐너의 연결 포트를 지정합니다. (Auto 또는 특정 COM 포트 선택)
+
Auto 선택 시 입력 수신 단자를 자동으로 자동 인식합니다.
4.2 설정 저장
@@ -1144,11 +968,26 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
모든 항목이 합격인 경우 (PASS):
모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
-
합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
-
마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다.
+
합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
+
마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
+
+
7.4 레이저 각인 QR 검증 팝업 및 결과 연동
+
레이저 마킹이 완료되면 검증을 위해 QR 검증 팝업 창이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
+
+
QR 스캔 검증 성공 (즉시 자동 닫힘)
+
+
스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 즉시 자동으로 닫히며 DB에 OK 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
+
+
+
QR 스캔 검증 실패 (NG 판정 및 설명 표출)
+
+
스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
+
팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 OK에서 NG (빨간색 테두리 및 배경)로 즉시 전환됩니다.
+
공정 진행 상태 및 시스템 상태 바에 "마킹 완료 / 각인 QR 검증 NG", "상태: 검사 불합격 (각인 QR 검증 실패)", "각인 QR 검증 실패 (DB NG 저장 완료)" 등 구체적인 원인 설명이 표시되며, DB에 최종 NG 결과로 기록됩니다.
+
8. 로그 및 데이터 관리
@@ -1249,6 +1088,15 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
+
9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오."
+
+
현상: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.
+
원인 및 해결:
+
네트워크/DB 서버 점검: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. Database.ini 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
+
계정 정보 확인: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 LoginOptions.json 계정 목록에 올바르게 등록되어 있는지 재확인하십시오.
+
+
+
9.2 프로그램 정상 종료 및 전원 자동 차단 규칙
본 설비는 고전력 계측 및 전원 제어를 함께 수행하므로 대기 전력 소모 방지와 안전을 위해 아래 절차에 따라 정상 종료 프로세스를 밟아야 합니다.
diff --git a/marking_gui/Models/Product.cs b/marking_gui/Models/Product.cs
index 8c2e7a8..432fb77 100644
--- a/marking_gui/Models/Product.cs
+++ b/marking_gui/Models/Product.cs
@@ -14,7 +14,7 @@ namespace marking_gui.Models
{
get
{
- bool housingOk = HousingResult == "PASS" || HousingResult == "OK";
+ bool housingOk = (HousingResult == "PASS" || HousingResult == "OK") && !string.IsNullOrWhiteSpace(PcbBarcode);
bool calOk = CalResult == "PASS" || CalResult == "OK";
bool eolOk = EolResult == "PASS" || EolResult == "OK";
return housingOk && calOk && eolOk;
diff --git a/marking_gui/PortSettingsWindow.xaml b/marking_gui/PortSettingsWindow.xaml
index e0e6018..82faaba 100644
--- a/marking_gui/PortSettingsWindow.xaml
+++ b/marking_gui/PortSettingsWindow.xaml
@@ -1,7 +1,7 @@
@@ -38,45 +38,78 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
diff --git a/marking_gui/PortSettingsWindow.xaml.cs b/marking_gui/PortSettingsWindow.xaml.cs
index f6108aa..6a9459f 100644
--- a/marking_gui/PortSettingsWindow.xaml.cs
+++ b/marking_gui/PortSettingsWindow.xaml.cs
@@ -1,7 +1,10 @@
using System;
using System.IO;
using System.IO.Ports;
+using System.Linq;
using System.Windows;
+using System.Windows.Controls;
+using marking_gui.Services;
namespace marking_gui
{
@@ -10,12 +13,16 @@ namespace marking_gui
///
public partial class PortSettingsWindow : Window
{
- private string _iniPath;
+ private string _sensorIniPath;
+ private string _settingIniPath;
+ public BarcodeScannerSettings ScannerSettings { get; private set; }
public PortSettingsWindow()
{
InitializeComponent();
- _iniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sensor.ini");
+ _sensorIniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sensor.ini");
+ _settingIniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
+ ScannerSettings = BarcodeScannerSettings.Load(_settingIniPath);
Loaded += PortSettingsWindow_Loaded;
}
@@ -23,14 +30,20 @@ namespace marking_gui
private void PortSettingsWindow_Loaded(object sender, RoutedEventArgs e)
{
// 1. 시스템에 유효한 COM 포트 목록 조회
- string[] ports = SerialPort.GetPortNames();
+ string[] ports = Array.Empty();
+ try
+ {
+ ports = SerialPort.GetPortNames().OrderBy(p => p).ToArray();
+ }
+ catch { }
+
+ // ── 4251 센서 포트 설정 바인딩 ──
cbPort.Items.Clear();
foreach (var port in ports)
{
cbPort.Items.Add(port);
}
- // 2. BaudRate 목록 추가
cbBaudRate.Items.Clear();
cbBaudRate.Items.Add("9600");
cbBaudRate.Items.Add("19200");
@@ -38,21 +51,19 @@ namespace marking_gui
cbBaudRate.Items.Add("57600");
cbBaudRate.Items.Add("115200");
- // 2-2. 채널 목록 추가 (1~4 채널)
cbChannel.Items.Clear();
cbChannel.Items.Add("1");
cbChannel.Items.Add("2");
- // 3. Sensor.ini 에서 현재 설정 로드
string currentPort = "COM3";
string currentBaud = "115200";
string currentChannel = "1";
- if (File.Exists(_iniPath))
+ if (File.Exists(_sensorIniPath))
{
try
{
- var lines = File.ReadAllLines(_iniPath);
+ var lines = File.ReadAllLines(_sensorIniPath);
foreach (var line in lines)
{
var trimmed = line.Trim();
@@ -75,34 +86,63 @@ namespace marking_gui
}
catch (Exception ex)
{
- System.Diagnostics.Debug.WriteLine("[PortSettings] 설정 로딩 중 오류: " + ex.Message);
+ System.Diagnostics.Debug.WriteLine("[PortSettings] 센서 설정 로딩 중 오류: " + ex.Message);
}
}
- // 4. 조회한 포트 목록에 현재 설정된 포트가 없으면 임시 추가
if (!cbPort.Items.Contains(currentPort))
{
cbPort.Items.Add(currentPort);
}
- // 5. 선택 상태 반영
cbPort.SelectedItem = currentPort;
- if (cbBaudRate.Items.Contains(currentBaud))
- {
- cbBaudRate.SelectedItem = currentBaud;
- }
- else
- {
- cbBaudRate.SelectedItem = "115200";
- }
+ if (cbBaudRate.Items.Contains(currentBaud)) cbBaudRate.SelectedItem = currentBaud;
+ else cbBaudRate.SelectedItem = "115200";
- if (cbChannel.Items.Contains(currentChannel))
+ if (cbChannel.Items.Contains(currentChannel)) cbChannel.SelectedItem = currentChannel;
+ else cbChannel.SelectedItem = "1";
+
+ // ── 바코드 / QR 스캐너 포트 설정 바인딩 ──
+ if (cbScannerPort != null)
{
- cbChannel.SelectedItem = currentChannel;
+ cbScannerPort.Items.Clear();
+ cbScannerPort.Items.Add("Auto");
+ foreach (var port in ports)
+ {
+ cbScannerPort.Items.Add(port);
+ }
+
+ string scannerPort = string.IsNullOrWhiteSpace(ScannerSettings.PortName) ? "Auto" : ScannerSettings.PortName.Trim();
+ bool foundPort = false;
+ foreach (var item in cbScannerPort.Items)
+ {
+ if (string.Equals(item.ToString(), scannerPort, StringComparison.OrdinalIgnoreCase))
+ {
+ cbScannerPort.SelectedItem = item;
+ foundPort = true;
+ break;
+ }
+ }
+ if (!foundPort) cbScannerPort.SelectedIndex = 0; // Auto
}
- else
+
+ if (cbScannerBaudRate != null)
{
- cbChannel.SelectedItem = "1";
+ string baudStr = ScannerSettings.BaudRate.ToString();
+ bool foundBaud = false;
+ foreach (ComboBoxItem item in cbScannerBaudRate.Items)
+ {
+ if (string.Equals(item.Content?.ToString(), baudStr, StringComparison.OrdinalIgnoreCase))
+ {
+ cbScannerBaudRate.SelectedItem = item;
+ foundBaud = true;
+ break;
+ }
+ }
+ if (!foundBaud && cbScannerBaudRate.Items.Count > 0)
+ {
+ cbScannerBaudRate.SelectedIndex = 0;
+ }
}
}
@@ -110,34 +150,45 @@ namespace marking_gui
{
if (cbPort.SelectedItem == null || cbBaudRate.SelectedItem == null || cbChannel.SelectedItem == null)
{
- MessageBox.Show("포트, 통신 속도 및 채널을 선택해 주십시오.", "알림", MessageBoxButton.OK, MessageBoxImage.Warning);
+ MessageBox.Show("센서 포트, 통신 속도 및 채널을 선택해 주십시오.", "알림", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
- string port = cbPort.SelectedItem.ToString();
- string baud = cbBaudRate.SelectedItem.ToString();
- string channel = cbChannel.SelectedItem.ToString();
+ string port = cbPort.SelectedItem.ToString() ?? "COM3";
+ string baud = cbBaudRate.SelectedItem.ToString() ?? "115200";
+ string channel = cbChannel.SelectedItem.ToString() ?? "1";
try
{
- // Sensor.ini 에 저장
- using (var sw = new StreamWriter(_iniPath, false))
+ // 1. Sensor.ini 에 저장
+ using (var sw = new StreamWriter(_sensorIniPath, false))
{
sw.WriteLine("; =================================================================");
sw.WriteLine("; 성능 검사 모니터링 시스템 - 4251 센서 하드웨어 통신 설정");
sw.WriteLine("; =================================================================");
sw.WriteLine();
sw.WriteLine("[Sensor]");
- sw.WriteLine("; 연결할 장치 센서 시리얼(COM) 포트명");
sw.WriteLine("Port=" + port);
- sw.WriteLine();
- sw.WriteLine("; 시리얼 통신 속도 (Baud Rate, 기본값: 115200)");
sw.WriteLine("BaudRate=" + baud);
- sw.WriteLine();
- sw.WriteLine("; 4251 센서 통신 채널 (기본값: 1, 범위: 1~4)");
sw.WriteLine("Channel=" + channel);
}
+ // 2. Setting.ini 의 [BarcodeScanner] 섹션에 바코드 스캐너 포트 저장
+ ScannerSettings.Enabled = true;
+ if (cbScannerPort != null && cbScannerPort.SelectedItem != null)
+ {
+ ScannerSettings.PortName = cbScannerPort.SelectedItem.ToString() ?? "Auto";
+ }
+ if (cbScannerBaudRate != null && cbScannerBaudRate.SelectedItem is ComboBoxItem selectedBaudItem)
+ {
+ if (int.TryParse(selectedBaudItem.Content?.ToString(), out int scannerBaud))
+ {
+ ScannerSettings.BaudRate = scannerBaud;
+ }
+ }
+
+ ScannerSettings.Save(_settingIniPath);
+
DialogResult = true;
Close();
}
@@ -147,6 +198,50 @@ 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
+ {
+ 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();
+
+ try { tempScanner?.Dispose(); } catch { }
+
+ if (testResult == true)
+ {
+ MessageBox.Show($"스캐너 (포트: {scannerPort}) 통신 및 QR 대조 검증 성공!\n\n현재 선택한 포트로 스캐너가 정상 작동합니다.", "스캔 테스트 성공", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ else
+ {
+ MessageBox.Show($"스캔 테스트가 중단되었거나 취소되었습니다.", "스캔 테스트 종료", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
diff --git a/marking_gui/QrVerificationWindow.xaml b/marking_gui/QrVerificationWindow.xaml
new file mode 100644
index 0000000..5347b0f
--- /dev/null
+++ b/marking_gui/QrVerificationWindow.xaml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/marking_gui/QrVerificationWindow.xaml.cs b/marking_gui/QrVerificationWindow.xaml.cs
new file mode 100644
index 0000000..817ab1a
--- /dev/null
+++ b/marking_gui/QrVerificationWindow.xaml.cs
@@ -0,0 +1,284 @@
+using System;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Media;
+using marking_gui.Services;
+
+namespace marking_gui
+{
+ public partial class QrVerificationWindow : Window
+ {
+ private readonly string _expectedQr;
+ private readonly SerialBarcodeScanner? _serialScanner;
+ private bool _isPassed = false;
+ private bool _hasScanned = false;
+ private bool _isClosing = false;
+ private readonly StringBuilder _keyScanBuffer = new StringBuilder();
+
+ public string ScannedQr { get; private set; } = string.Empty;
+
+ public QrVerificationWindow(string expectedQr, SerialBarcodeScanner? serialScanner = null)
+ {
+ InitializeComponent();
+ _expectedQr = expectedQr ?? string.Empty;
+ _serialScanner = serialScanner;
+ }
+
+ private void Window_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (txtExpectedQr != null)
+ {
+ txtExpectedQr.Text = _expectedQr;
+ }
+
+ if (_serialScanner != null)
+ {
+ _serialScanner.ScanReceived += SerialScanner_ScanReceived;
+ }
+
+ ResetToWaitingState();
+ }
+
+ private void Window_Unloaded(object sender, RoutedEventArgs e)
+ {
+ if (_serialScanner != null)
+ {
+ _serialScanner.ScanReceived -= SerialScanner_ScanReceived;
+ }
+ }
+
+ private void SerialScanner_ScanReceived(object? sender, BarcodeScanReceivedEventArgs e)
+ {
+ Dispatcher.BeginInvoke(new Action(async () =>
+ {
+ if (!string.IsNullOrWhiteSpace(e.Text))
+ {
+ _serialScanner?.ConfirmPort(e.PortName);
+ await ProcessScanResultAsync(e.Text.Trim());
+ }
+ }), System.Windows.Threading.DispatcherPriority.Input);
+ }
+
+ private void ResetToWaitingState()
+ {
+ _hasScanned = false;
+ _isPassed = false;
+ _isClosing = false;
+ _keyScanBuffer.Clear();
+
+ if (txtExpectedQr != null)
+ {
+ txtExpectedQr.Text = _expectedQr;
+ }
+
+ if (txtScannedQr != null)
+ {
+ txtScannedQr.Text = "스캔 대기 중...";
+ txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(148, 163, 184)); // Gray
+ }
+
+ if (txtScanStatusSubText != null)
+ {
+ txtScanStatusSubText.Text = "(스캔 대기 중)";
+ txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(148, 163, 184));
+ }
+
+ if (borderExpectedCard != null)
+ {
+ borderExpectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
+ borderExpectedCard.BorderThickness = new Thickness(1.5);
+ }
+
+ if (borderScannedCard != null)
+ {
+ borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(203, 213, 225)); // Gray
+ borderScannedCard.BorderThickness = new Thickness(1.5);
+ }
+
+ if (borderStatusBanner != null)
+ {
+ borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(241, 245, 249)); // Light Gray
+ }
+
+ if (txtStatusMessage != null)
+ {
+ txtStatusMessage.Text = "스캔 대기 중";
+ txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(51, 65, 85));
+ }
+ }
+
+ private async void Window_PreviewKeyDown(object sender, KeyEventArgs e)
+ {
+ // ESC 키로 창 닫기
+ if (e.Key == Key.Escape)
+ {
+ btnCancel_Click(sender, e);
+ return;
+ }
+
+ // Enter 키 수신 시 입력된 스캔 버퍼 전송
+ if (e.Key == Key.Enter || e.Key == Key.Return)
+ {
+ string scannedText = _keyScanBuffer.ToString().Trim();
+ _keyScanBuffer.Clear();
+
+ if (!string.IsNullOrEmpty(scannedText))
+ {
+ await ProcessScanResultAsync(scannedText);
+ }
+ return;
+ }
+
+ // 일반 문자 키 수집 (스캐너 가상 키보드 수신)
+ char keyChar = KeyToChar(e.Key);
+ if (keyChar != '\0')
+ {
+ _keyScanBuffer.Append(keyChar);
+ }
+ }
+
+ private char KeyToChar(Key key)
+ {
+ if (key >= Key.D0 && key <= Key.D9) return (char)('0' + (key - Key.D0));
+ if (key >= Key.NumPad0 && key <= Key.NumPad9) return (char)('0' + (key - Key.NumPad0));
+ if (key >= Key.A && key <= Key.Z) return (char)('A' + (key - Key.A));
+ if (key == Key.OemSemicolon) return ';';
+ if (key == Key.OemMinus) return '-';
+ return '\0';
+ }
+
+ private async Task ProcessScanResultAsync(string scannedText)
+ {
+ if (_isClosing) return;
+
+ _hasScanned = true;
+ ScannedQr = scannedText ?? string.Empty;
+
+ if (txtScannedQr != null)
+ {
+ txtScannedQr.Text = scannedText;
+ }
+
+ // nnn;nnnn;nnnn;nnnn; (세미콜론 구분 포맷 정밀 비교)
+ string normExpected = NormalizeQrFormat(_expectedQr);
+ string normScanned = NormalizeQrFormat(scannedText);
+
+ bool isMatch = string.Equals(normScanned, normExpected, StringComparison.OrdinalIgnoreCase);
+
+ if (isMatch)
+ {
+ _isPassed = true;
+
+ if (txtScannedQr != null)
+ {
+ txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Green
+ }
+
+ if (txtScanStatusSubText != null)
+ {
+ txtScanStatusSubText.Text = "(스캔 완료 - 일치)";
+ txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74));
+ }
+
+ if (borderExpectedCard != null)
+ {
+ borderExpectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(34, 197, 94)); // Green
+ }
+
+ if (borderScannedCard != null)
+ {
+ borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(34, 197, 94)); // Green
+ borderScannedCard.BorderThickness = new Thickness(2);
+ }
+
+ if (borderStatusBanner != null)
+ {
+ borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(240, 253, 244)); // Light Green
+ }
+
+ if (txtStatusMessage != null)
+ {
+ txtStatusMessage.Text = "QR 코드 검증 성공";
+ txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green
+ }
+
+ // 스캔값이 합격이면 딜레이 없이 즉시 팝업 닫기
+ if (!_isClosing)
+ {
+ _isClosing = true;
+ DialogResult = true;
+ Close();
+ }
+ }
+ else
+ {
+ _isPassed = false;
+
+ if (txtScannedQr != null)
+ {
+ txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
+ }
+
+ if (txtScanStatusSubText != null)
+ {
+ txtScanStatusSubText.Text = "(스캔 완료 - 불일치)";
+ txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38));
+ }
+
+ if (borderScannedCard != null)
+ {
+ borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
+ borderScannedCard.BorderThickness = new Thickness(2);
+ }
+
+ if (borderStatusBanner != null)
+ {
+ borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(254, 242, 242)); // Light Red
+ }
+
+ if (txtStatusMessage != null)
+ {
+ txtStatusMessage.Text = "QR 바코드 불일치";
+ txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
+ }
+ }
+ }
+
+ private static string NormalizeQrFormat(string input)
+ {
+ if (string.IsNullOrWhiteSpace(input)) return string.Empty;
+
+ string trimmed = input.Trim();
+ if (!trimmed.EndsWith(";"))
+ {
+ trimmed += ";";
+ }
+ return trimmed;
+ }
+
+ private void btnCancel_Click(object sender, RoutedEventArgs e)
+ {
+ if (_isClosing) return;
+ _isClosing = true;
+ DialogResult = false;
+ Close();
+ }
+
+ private void btnConfirm_Click(object sender, RoutedEventArgs e)
+ {
+ if (_isClosing) return;
+
+ if (!_hasScanned)
+ {
+ MessageBox.Show("QR 코드를 스캔한 후 확인 버튼을 클릭하십시오.", "스캔 대기", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
+
+ _isClosing = true;
+ DialogResult = _isPassed;
+ Close();
+ }
+ }
+}
diff --git a/marking_gui/Services/BarcodeScannerSettings.cs b/marking_gui/Services/BarcodeScannerSettings.cs
new file mode 100644
index 0000000..d373cc5
--- /dev/null
+++ b/marking_gui/Services/BarcodeScannerSettings.cs
@@ -0,0 +1,163 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace marking_gui.Services
+{
+ public sealed class BarcodeScannerSettings
+ {
+ public bool Enabled { get; set; } = true;
+ public string PortName { get; set; } = "Auto";
+ public int BaudRate { get; set; } = 9600;
+ public int ReadTimeout { get; set; } = 500;
+ public int IdleCommitMilliseconds { get; set; } = 250;
+ public bool DtrEnable { get; set; } = true;
+ public bool RtsEnable { get; set; } = true;
+
+ public static bool IsAutoPort(string? portName)
+ {
+ return string.IsNullOrWhiteSpace(portName) ||
+ string.Equals(portName.Trim(), "Auto", StringComparison.OrdinalIgnoreCase);
+ }
+
+ public static BarcodeScannerSettings Load(string iniPath)
+ {
+ var settings = new BarcodeScannerSettings();
+
+ if (!File.Exists(iniPath))
+ {
+ return settings;
+ }
+
+ try
+ {
+ var lines = File.ReadAllLines(iniPath);
+ bool isScannerSection = false;
+
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+ if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith(";") || trimmed.StartsWith("#"))
+ {
+ continue;
+ }
+
+ if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
+ {
+ string sectionName = trimmed.Substring(1, trimmed.Length - 2).Trim();
+ isScannerSection = string.Equals(sectionName, "BarcodeScanner", StringComparison.OrdinalIgnoreCase);
+ continue;
+ }
+
+ if (!isScannerSection)
+ {
+ continue;
+ }
+
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length != 2)
+ {
+ continue;
+ }
+
+ string key = parts[0].Trim();
+ string val = parts[1].Trim();
+
+ if (string.Equals(key, "Enabled", StringComparison.OrdinalIgnoreCase))
+ {
+ if (bool.TryParse(val, out bool enabled)) settings.Enabled = enabled;
+ }
+ else if (string.Equals(key, "PortName", StringComparison.OrdinalIgnoreCase))
+ {
+ settings.PortName = val;
+ }
+ else if (string.Equals(key, "BaudRate", StringComparison.OrdinalIgnoreCase))
+ {
+ if (int.TryParse(val, out int baudRate)) settings.BaudRate = baudRate;
+ }
+ else if (string.Equals(key, "ReadTimeout", StringComparison.OrdinalIgnoreCase))
+ {
+ if (int.TryParse(val, out int timeout)) settings.ReadTimeout = timeout;
+ }
+ else if (string.Equals(key, "IdleCommitMilliseconds", StringComparison.OrdinalIgnoreCase))
+ {
+ if (int.TryParse(val, out int idleMs)) settings.IdleCommitMilliseconds = idleMs;
+ }
+ else if (string.Equals(key, "DtrEnable", StringComparison.OrdinalIgnoreCase))
+ {
+ if (bool.TryParse(val, out bool dtr)) settings.DtrEnable = dtr;
+ }
+ else if (string.Equals(key, "RtsEnable", StringComparison.OrdinalIgnoreCase))
+ {
+ if (bool.TryParse(val, out bool rts)) settings.RtsEnable = rts;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[BarcodeScannerSettings Load Error] " + ex.Message);
+ }
+
+ return settings;
+ }
+
+ public void Save(string iniPath)
+ {
+ try
+ {
+ var lines = File.Exists(iniPath) ? new List(File.ReadAllLines(iniPath)) : new List();
+ int sectionIndex = -1;
+ int nextSectionIndex = lines.Count;
+
+ for (int i = 0; i < lines.Count; i++)
+ {
+ string trimmed = lines[i].Trim();
+ if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
+ {
+ string sec = trimmed.Substring(1, trimmed.Length - 2).Trim();
+ if (string.Equals(sec, "BarcodeScanner", StringComparison.OrdinalIgnoreCase))
+ {
+ sectionIndex = i;
+ }
+ else if (sectionIndex >= 0 && nextSectionIndex == lines.Count)
+ {
+ nextSectionIndex = i;
+ }
+ }
+ }
+
+ var newSectionLines = new List
+ {
+ "[BarcodeScanner]",
+ $"Enabled={Enabled.ToString().ToLower()}",
+ $"PortName={PortName}",
+ $"BaudRate={BaudRate}",
+ $"ReadTimeout={ReadTimeout}",
+ $"IdleCommitMilliseconds={IdleCommitMilliseconds}",
+ $"DtrEnable={DtrEnable.ToString().ToLower()}",
+ $"RtsEnable={RtsEnable.ToString().ToLower()}"
+ };
+
+ if (sectionIndex >= 0)
+ {
+ lines.RemoveRange(sectionIndex, nextSectionIndex - sectionIndex);
+ lines.InsertRange(sectionIndex, newSectionLines);
+ }
+ else
+ {
+ if (lines.Count > 0 && !string.IsNullOrWhiteSpace(lines[lines.Count - 1]))
+ {
+ lines.Add("");
+ }
+ lines.AddRange(newSectionLines);
+ }
+
+ File.WriteAllLines(iniPath, lines);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[BarcodeScannerSettings Save Error] " + ex.Message);
+ }
+ }
+ }
+}
diff --git a/marking_gui/Services/DatabaseService.cs b/marking_gui/Services/DatabaseService.cs
index fdc147e..b2dce66 100644
--- a/marking_gui/Services/DatabaseService.cs
+++ b/marking_gui/Services/DatabaseService.cs
@@ -120,13 +120,13 @@ namespace marking_gui.Services
_connectionString += "TrustServerCertificate=True;";
}
- // DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 3초로 강제 조정
+ // DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 1초로 강제 조정하고 재시도 비활성화
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;";
+ _connectionString += "Connect Timeout=1;Connect Retry Count=0;";
}
}
catch (Exception ex)
@@ -222,6 +222,38 @@ namespace marking_gui.Services
return null;
}
+ ///
+ /// [dbo].[Marking] 테이블에 해당 IC S/N 데이터가 이미 존재하는지 중복 검사를 수행합니다.
+ ///
+ 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;
+ }
+ }
+
///
/// 최종 성능 검출 값을 데이터베이스에 저장합니다.
/// 전압과 전류 수치는 3,2 포맷(DECIMAL(3, 2) 등)에 대응하기 위해 소수점 이하 2자리로 반올림하여 처리합니다.
@@ -261,13 +293,7 @@ namespace marking_gui.Services
{
serial = serial.Substring(0, 30);
}
- // PCB_Barcode: Housing_Assembly 테이블에서 가져온 값 우선 사용, 없으면 IC_SN으로 대체
- string safePcbBarcode = !string.IsNullOrWhiteSpace(pcbBarcode) ? pcbBarcode : serial;
- if (safePcbBarcode.Length > 30) safePcbBarcode = safePcbBarcode.Substring(0, 30);
- string safeProcessLabel = "Marking";
- string safeProcessLabelScan = string.IsNullOrEmpty(result) ? "OK" : (result.Length > 5 ? result.Substring(0, 5) : result);
string safeResult = string.IsNullOrEmpty(result) ? "NG" : (result.Length > 5 ? result.Substring(0, 5) : result);
- string safeErrorCode = (safeResult == "OK" || safeResult == "PASS") ? "0" : "E01";
// DB 컬럼 길이 한계에 맞춰서 문자열 안전하게 자르기 (Truncate)
string safeMaker = string.IsNullOrEmpty(maker) ? "" : (maker.Length > 10 ? maker.Substring(0, 10) : maker);
@@ -322,24 +348,23 @@ namespace marking_gui.Services
[Production_Date] = @ProdDate,
[Line] = @Line,
[Lot_No] = @LotNo,
- [JIG_No] = @JigNo,
- [PCB_Barcode] = @PcbBarcode,
- [Process_Label] = @ProcessLabel,
- [Process_Label_Scan] = @ProcessLabelScan,
+ [Jig_No] = @JigNo,
[PT_Vol_1] = @Vol1,
[PT_Current_1] = @Cur1,
+ [PT_Resistance_1] = @Res1,
[Marking_QR] = @Qr,
- [Result] = @Result,
- [Error_Code] = @ErrorCode
+ [Result] = @Result
WHEN NOT MATCHED THEN
- INSERT ([Maker], [Model], [Variant_1], [Variant_2], [Operator], [Production_Date], [Line], [Lot_No], [JIG_No], [IC_SN], [PCB_Barcode], [Process_Label], [Process_Label_Scan], [PT_Vol_1], [PT_Current_1], [Marking_QR], [Result], [Error_Code])
- VALUES (@Maker, @Model, @Var1, @Var2, @Operator, @ProdDate, @Line, @LotNo, @JigNo, @Serial, @PcbBarcode, @ProcessLabel, @ProcessLabelScan, @Vol1, @Cur1, @Qr, @Result, @ErrorCode);";
+ INSERT ([IC_SN], [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, @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("@Qr", markingQr);
cmd.Parameters.AddWithValue("@Maker", safeMaker);
cmd.Parameters.AddWithValue("@Model", safeModel);
cmd.Parameters.AddWithValue("@Var1", safeVariant1);
@@ -349,18 +374,12 @@ namespace marking_gui.Services
cmd.Parameters.AddWithValue("@Line", safeLine);
cmd.Parameters.AddWithValue("@LotNo", safeLotNo);
cmd.Parameters.AddWithValue("@JigNo", safeJigNo);
- cmd.Parameters.AddWithValue("@Serial", serial);
- cmd.Parameters.AddWithValue("@PcbBarcode", safePcbBarcode);
- cmd.Parameters.AddWithValue("@ProcessLabel", safeProcessLabel);
- cmd.Parameters.AddWithValue("@ProcessLabelScan", safeProcessLabelScan);
- cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Vol1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 2, Value = roundedVoltage });
- cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Cur1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 2, Value = roundedCurrent });
-
- cmd.Parameters.AddWithValue("@Qr", markingQr);
+ cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Vol1", System.Data.SqlDbType.Decimal) { Precision = 4, Scale = 2, Value = roundedVoltage });
+ cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Cur1", System.Data.SqlDbType.Decimal) { Precision = 4, Scale = 2, Value = roundedCurrent });
+ cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Res1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 3, Value = roundedResistance });
cmd.Parameters.AddWithValue("@Result", safeResult);
- cmd.Parameters.AddWithValue("@ErrorCode", safeErrorCode);
-
+
conn.Open();
cmd.ExecuteNonQuery();
}
diff --git a/marking_gui/Services/LoginServicesStub.cs b/marking_gui/Services/LoginServicesStub.cs
index 3e20547..43371fe 100644
--- a/marking_gui/Services/LoginServicesStub.cs
+++ b/marking_gui/Services/LoginServicesStub.cs
@@ -44,7 +44,7 @@ namespace Housing.Services
connStr.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0)
{
if (!connStr.EndsWith(";")) connStr += ";";
- connStr += "Connect Timeout=3;";
+ connStr += "Connect Timeout=1;Connect Retry Count=0;";
}
return connStr;
@@ -338,10 +338,10 @@ namespace Housing.Services
return true;
}
- // DB 커넥션 스트링이 비어있으면 기본값 확인
+ // DB 커넥션 스트링이 비어있으면 로그인 불가
if (string.IsNullOrWhiteSpace(_dbSettings.ConnectionString))
{
- return IsDefaultAccount(operatorId, password);
+ return false;
}
try
@@ -399,42 +399,15 @@ namespace Housing.Services
}
catch (Exception ex)
{
- // DB 연결이나 쿼리 오류 발생 시 로그 출력 후 디폴트 계정 검증으로 fallback
- System.Diagnostics.Debug.WriteLine($"로그인 검증 중 에러 발생: {ex.Message}");
-
- // ==================== [DEMO MODE START] ====================
- // DB가 없고 데모 환경일 때도 프로시저를 호출하려는 동작 및 로그 연출을 수행한 후 강제로 성공 처리
- // 상용 릴리즈 시 이 블록은 완전히 제거(삭제)할 수 있습니다.
- if (!string.IsNullOrWhiteSpace(_loginSettings.Procedure))
- {
- System.Diagnostics.Debug.WriteLine(string.Format("[데모 시뮬레이션] 프로시저 '{0}' 호출 모사 (ID: {1}, PW: {2}, Process: {3})",
- _loginSettings.Procedure, operatorId, password, _loginSettings.ProcessName));
- return IsDefaultAccount(operatorId, password);
- }
- // ==================== [DEMO MODE END] ====================
-
- return IsDefaultAccount(operatorId, password);
+ // DB 연결이나 쿼리 오류 발생 시 상위로 예외 전달 (DB 연결 상태 확인 안내 팝업 표출을 위함)
+ System.Diagnostics.Debug.WriteLine($"로그인 DB 검증 중 에러 발생: {ex.Message}");
+ throw;
}
}
private bool IsDefaultAccount(string operatorId, string password)
{
- // 기본 개발/테스트용 계정 처리
- if (string.Equals(operatorId, "test", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(password, "test", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- if (string.Equals(operatorId, "admin", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(password, "admin", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- if (string.Equals(operatorId, "su", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(password, "su", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
+ // 하드코딩 테스트 계정 완전 제거 (LoginOptions.json 및 DB 계정만 사용)
return false;
}
}
diff --git a/marking_gui/Services/SerialBarcodeScanner.cs b/marking_gui/Services/SerialBarcodeScanner.cs
new file mode 100644
index 0000000..3c51632
--- /dev/null
+++ b/marking_gui/Services/SerialBarcodeScanner.cs
@@ -0,0 +1,263 @@
+using System;
+using System.Collections.Generic;
+using System.IO.Ports;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace marking_gui.Services
+{
+ public class BarcodeScanReceivedEventArgs : EventArgs
+ {
+ public string PortName { get; }
+ public string Text { get; }
+ public bool IsConfirmedPort { get; }
+
+ public BarcodeScanReceivedEventArgs(string portName, string text, bool isConfirmedPort)
+ {
+ PortName = portName;
+ Text = text;
+ IsConfirmedPort = isConfirmedPort;
+ }
+ }
+
+ public sealed class SerialBarcodeScanner : IDisposable
+ {
+ private sealed class PortSession : IDisposable
+ {
+ public SerialPort Port { get; }
+ public StringBuilder Buffer { get; } = new StringBuilder();
+ public Timer? Timer { get; set; }
+ private readonly Action _onIdleCommit;
+
+ public PortSession(SerialPort port, Action onIdleCommit)
+ {
+ Port = port;
+ _onIdleCommit = onIdleCommit;
+ }
+
+ public void ResetIdleTimer(int milliseconds)
+ {
+ if (Timer == null)
+ {
+ Timer = new Timer(_ => _onIdleCommit(this), null, milliseconds, Timeout.Infinite);
+ }
+ else
+ {
+ Timer.Change(milliseconds, Timeout.Infinite);
+ }
+ }
+
+ public void Dispose()
+ {
+ Timer?.Dispose();
+ try
+ {
+ if (Port.IsOpen)
+ {
+ Port.Close();
+ }
+ Port.Dispose();
+ }
+ catch { }
+ }
+ }
+
+ private readonly BarcodeScannerSettings _settings;
+ private readonly HashSet _excludedPortNames;
+ private readonly List _sessions = new List();
+ private string? _confirmedPortName;
+ private bool _disposed;
+
+ public event EventHandler? ScanReceived;
+
+ public bool IsRunning => _sessions.Count > 0;
+ public string? ConfirmedPortName => _confirmedPortName;
+
+ public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable? excludedPortNames = null)
+ {
+ _settings = settings ?? new BarcodeScannerSettings();
+ _excludedPortNames = new HashSet(excludedPortNames ?? Array.Empty(), StringComparer.OrdinalIgnoreCase);
+ }
+
+ public void Start()
+ {
+ if (!_settings.Enabled) return;
+
+ Stop();
+
+ var candidatePorts = GetCandidatePortNames();
+ if (candidatePorts.Length == 0)
+ {
+ System.Diagnostics.Debug.WriteLine("[SerialBarcodeScanner] 사용 가능한 COM 포트가 없습니다.");
+ return;
+ }
+
+ foreach (var portName in candidatePorts)
+ {
+ try
+ {
+ var port = CreateSerialPort(portName);
+ var session = new PortSession(port, CommitBufferedScan);
+
+ port.DataReceived += SerialPort_DataReceived;
+ _sessions.Add(session);
+
+ port.Open();
+ port.DiscardInBuffer();
+ port.DiscardOutBuffer();
+
+ System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] 시리얼 스캐너 포트 오픈 성공: {portName}");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] 포트 '{portName}' 오픈 실패: {ex.Message}");
+ }
+ }
+ }
+
+ public void Stop()
+ {
+ foreach (var session in _sessions)
+ {
+ try
+ {
+ session.Port.DataReceived -= SerialPort_DataReceived;
+ session.Dispose();
+ }
+ catch { }
+ }
+ _sessions.Clear();
+ _confirmedPortName = null;
+ }
+
+ public void ConfirmPort(string portName)
+ {
+ if (string.IsNullOrWhiteSpace(portName)) return;
+
+ string normalized = NormalizePortName(portName);
+ if (string.Equals(_confirmedPortName, normalized, StringComparison.OrdinalIgnoreCase)) return;
+
+ _confirmedPortName = normalized;
+
+ // 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
+ for (int i = _sessions.Count - 1; i >= 0; i--)
+ {
+ var session = _sessions[i];
+ if (!string.Equals(session.Port.PortName, normalized, StringComparison.OrdinalIgnoreCase))
+ {
+ try
+ {
+ session.Port.DataReceived -= SerialPort_DataReceived;
+ session.Dispose();
+ }
+ catch { }
+ _sessions.RemoveAt(i);
+ }
+ }
+ }
+
+ private SerialPort CreateSerialPort(string portName)
+ {
+ return new SerialPort(portName, _settings.BaudRate)
+ {
+ Encoding = Encoding.ASCII,
+ NewLine = "\r\n",
+ ReadTimeout = _settings.ReadTimeout,
+ WriteTimeout = _settings.ReadTimeout,
+ DtrEnable = _settings.DtrEnable,
+ RtsEnable = _settings.RtsEnable
+ };
+ }
+
+ private string[] GetCandidatePortNames()
+ {
+ if (!BarcodeScannerSettings.IsAutoPort(_settings.PortName))
+ {
+ return new[] { NormalizePortName(_settings.PortName) };
+ }
+
+ try
+ {
+ return SerialPort.GetPortNames()
+ .Select(NormalizePortName)
+ .Where(p => !_excludedPortNames.Contains(p))
+ .OrderBy(GetPortNumber)
+ .ToArray();
+ }
+ catch
+ {
+ return Array.Empty();
+ }
+ }
+
+ private static string NormalizePortName(string portName)
+ {
+ if (string.IsNullOrWhiteSpace(portName)) return string.Empty;
+ return portName.Trim().ToUpper();
+ }
+
+ private static int GetPortNumber(string portName)
+ {
+ if (portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase) &&
+ int.TryParse(portName.Substring(3), out int num))
+ {
+ return num;
+ }
+ return int.MaxValue;
+ }
+
+ private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
+ {
+ if (sender is not SerialPort port) return;
+
+ var session = _sessions.FirstOrDefault(s => ReferenceEquals(s.Port, port));
+ if (session == null) return;
+
+ try
+ {
+ string text = port.ReadExisting();
+ if (string.IsNullOrEmpty(text)) return;
+
+ lock (session.Buffer)
+ {
+ session.Buffer.Append(text);
+ }
+
+ session.ResetIdleTimer(_settings.IdleCommitMilliseconds);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[SerialPort_DataReceived Error] {port.PortName}: {ex.Message}");
+ }
+ }
+
+ private void CommitBufferedScan(PortSession session)
+ {
+ string scanText;
+ lock (session.Buffer)
+ {
+ scanText = session.Buffer.ToString();
+ session.Buffer.Clear();
+ }
+
+ if (string.IsNullOrWhiteSpace(scanText)) return;
+
+ scanText = scanText.Trim();
+
+ bool isConfirmed = string.Equals(_confirmedPortName, session.Port.PortName, StringComparison.OrdinalIgnoreCase);
+
+ ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed));
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ Stop();
+ _disposed = true;
+ }
+ }
+ }
+}
diff --git a/marking_gui/Setting.ini b/marking_gui/Setting.ini
index 09e79f1..a2aa537 100644
--- a/marking_gui/Setting.ini
+++ b/marking_gui/Setting.ini
@@ -6,8 +6,8 @@
; 마킹 제어 라이브러리 및 드라이버(EzCad) 폴더 경로
EzdFolderPath=c:\EzCad
-; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출)
-test=false
+; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출 반대 false)
+test=true
; 도면품번 (EZD 도안의 도면품번 태그에 채워질 값 — DrawingNoTag에 매핑)
DrawingNo=APTSL04A
@@ -111,3 +111,18 @@ MaxCurrent=10.00
MinResistance=8.000
MaxResistance=13.000
+
+; =================================================================
+; 바코드 / QR 스캐너 연동 설정 (RS232 / USB Virtual COM / Auto)
+; =================================================================
+
+[BarcodeScanner]
+Enabled=true
+PortName=Auto
+BaudRate=9600
+ReadTimeout=500
+IdleCommitMilliseconds=250
+DtrEnable=true
+RtsEnable=true
+
+
diff --git a/marking_gui/SettingsWindow.xaml b/marking_gui/SettingsWindow.xaml
index 7b2c831..fa28a4c 100644
--- a/marking_gui/SettingsWindow.xaml
+++ b/marking_gui/SettingsWindow.xaml
@@ -1,7 +1,7 @@
@@ -45,42 +45,49 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/marking_gui/SettingsWindow.xaml.cs b/marking_gui/SettingsWindow.xaml.cs
index 8f492d8..15355a3 100644
--- a/marking_gui/SettingsWindow.xaml.cs
+++ b/marking_gui/SettingsWindow.xaml.cs
@@ -15,7 +15,6 @@ namespace marking_gui
{
InitializeComponent();
- // 원본의 데이터 무결성을 유지하기 위해 임시 복제하여 사용
Limits = new MeasurementLimits(limits.MinVoltage, limits.MaxVoltage, limits.MinCurrent, limits.MaxCurrent, limits.MinResistance, limits.MaxResistance);
txtMinVoltage.Text = Limits.MinVoltage.ToString("F1");
@@ -50,6 +49,7 @@ namespace marking_gui
}
Limits = tempLimits;
+
DialogResult = true;
Close();
}
diff --git a/marking_gui/StartupLoginWindow.xaml.cs b/marking_gui/StartupLoginWindow.xaml.cs
index a560e79..545ed9c 100644
--- a/marking_gui/StartupLoginWindow.xaml.cs
+++ b/marking_gui/StartupLoginWindow.xaml.cs
@@ -49,6 +49,13 @@ namespace Housing.Login
MakerComboBox.Focus();
}
+ private enum LoginCheckStatus
+ {
+ Success,
+ InvalidCredentials,
+ DbTimeoutOrError
+ }
+
private async void LogInButton_Click(object sender, RoutedEventArgs e)
{
CloseHistoryPopup();
@@ -63,15 +70,26 @@ namespace Housing.Login
button.IsEnabled = false;
}
+ System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
+
try
{
- if (!await IsValidLoginAsync(loginResult.Operator, password))
+ var checkResult = await CheckLoginAsync(loginResult.Operator, password);
+
+ if (checkResult == LoginCheckStatus.InvalidCredentials)
{
MessageBox.Show(this, "등록되지 않은 ID/PW입니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error);
PasswordBox.Clear();
PasswordBox.Focus();
return;
}
+ else if (checkResult == LoginCheckStatus.DbTimeoutOrError)
+ {
+ MessageBox.Show(this, "DB 연결 상태 또는 ID/PW를 확인해 주십시오.", "로그인 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
+ PasswordBox.Clear();
+ PasswordBox.Focus();
+ return;
+ }
loginResult.Password = password; // 비밀번호를 결과 객체에도 담아둠
Result = loginResult;
@@ -87,6 +105,7 @@ namespace Housing.Login
}
finally
{
+ System.Windows.Input.Mouse.OverrideCursor = null;
if (sender is Button loginButton)
{
loginButton.IsEnabled = true;
@@ -94,7 +113,7 @@ namespace Housing.Login
}
}
- private async Task IsValidLoginAsync(string loginId, string password)
+ private async Task CheckLoginAsync(string loginId, string password)
{
// 1. LoginOptions.json의 로컬 계정 정보에서 먼저 찾기
if (_loginOptions.LoginAccounts != null && _loginOptions.LoginAccounts.Count > 0)
@@ -105,22 +124,44 @@ namespace Housing.Login
if (matched)
{
- return true;
+ return LoginCheckStatus.Success;
}
}
- // 2. 일치하는 계정이 없다면 기존 DB 조회 로직 태우기
+ // 2. 오프라인 모드일 경우 통과
var loginSettings = LoginSettings.Load(GetDatabaseIniPath());
if (loginSettings.OfflinePreview)
{
- return true;
+ return LoginCheckStatus.Success;
}
- var repository = new LoginAccountRepository(
- DatabaseSettings.Load(GetDatabaseIniPath()),
- loginSettings);
+ // 3. DB 접속 및 계정 조회 (1초 하드 제한)
+ try
+ {
+ var repository = new LoginAccountRepository(
+ DatabaseSettings.Load(GetDatabaseIniPath()),
+ loginSettings);
- return await repository.ExistsAsync(loginId, password);
+ var dbCheckTask = repository.ExistsAsync(loginId, password);
+ var timeoutTask = Task.Delay(1000); // 1초 하드 제한
+
+ var completedTask = await Task.WhenAny(dbCheckTask, timeoutTask);
+ if (completedTask == dbCheckTask)
+ {
+ bool isExist = await dbCheckTask;
+ return isExist ? LoginCheckStatus.Success : LoginCheckStatus.InvalidCredentials;
+ }
+ else
+ {
+ System.Diagnostics.Debug.WriteLine("[로그인 타임아웃] DB 응답 1초 초과 -> 타임아웃/DB연결안내 팝업 표출");
+ return LoginCheckStatus.DbTimeoutOrError;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[로그인 오류] DB 연결 예외 발생: {ex.Message}");
+ return LoginCheckStatus.DbTimeoutOrError;
+ }
}
private bool TryReadRequiredFields(out StartupLoginWindowResult result, out string password)
@@ -293,24 +334,55 @@ namespace Housing.Login
foreach (var value in values)
{
- var item = new Button
+ var rowGrid = new Grid
+ {
+ Width = menuWidth - 2,
+ Background = textBox.Background
+ };
+ rowGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+ rowGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
+
+ var selectBtn = new Button
{
Content = value,
- MinWidth = menuWidth,
Padding = new Thickness(10, 6, 10, 6),
BorderThickness = new Thickness(0),
- Background = textBox.Background,
+ Background = System.Windows.Media.Brushes.Transparent,
Foreground = textBox.Foreground,
HorizontalContentAlignment = HorizontalAlignment.Left
};
- item.Click += (_, _) =>
+ selectBtn.Click += (_, _) =>
{
textBox.Text = value;
textBox.CaretIndex = textBox.Text.Length;
CloseHistoryPopup();
FocusControl(nextFocusControl);
};
- stackPanel.Children.Add(item);
+ Grid.SetColumn(selectBtn, 0);
+ rowGrid.Children.Add(selectBtn);
+
+ var deleteBtn = new Button
+ {
+ Content = "✕",
+ Width = 28,
+ Height = 28,
+ Padding = new Thickness(0),
+ BorderThickness = new Thickness(0),
+ Background = System.Windows.Media.Brushes.Transparent,
+ Foreground = System.Windows.Media.Brushes.Red,
+ FontWeight = FontWeights.Bold,
+ ToolTip = "이력 삭제"
+ };
+ deleteBtn.Click += (s, e) =>
+ {
+ _loginHistoryStore.Forget(fieldName, value);
+ _loginHistoryStore.Save();
+ ShowHistoryMenu(textBox, fieldName, nextFocusControl);
+ };
+ Grid.SetColumn(deleteBtn, 1);
+ rowGrid.Children.Add(deleteBtn);
+
+ stackPanel.Children.Add(rowGrid);
}
popup.Child = new Border
diff --git a/program_flow.md b/program_flow.md
index 10e475e..847bd80 100644
--- a/program_flow.md
+++ b/program_flow.md
@@ -6,11 +6,11 @@
## 1. 시스템 개요 (Introduction)
-본 시스템은 제조 라인의 최종 단계에서 완제품의 3대 전기적 성능(전압, 전류, 저항)을 통합 검사하고, 적합 제품에 한해 표면에 2D QR 코드 및 일련번호를 레이저 마킹하는 자동화 모니터링 프로그램입니다.
+본 시스템은 제조 라인의 최종 단계에서 완제품의 3대 전기적 성능(전압, 전류, 저항)을 통합 검사하고, 적합 제품에 한해 표면에 QR 코드 및 일련번호를 레이저 마킹하는 자동화 모니터링 프로그램입니다.
- **선행 공정 교차 검증:** 제품 바코드(S/N)를 수집하여 이전 주요 공정(Housing, Calibration, EOL)을 모두 통과했는지 생산 데이터베이스에서 자동 조회합니다.
- **실시간 3대 파라미터 계측:** 전원 공급 장치 및 DMM 계측기와 연동하여 전압(V), 전류(mA), 저항(kΩ/Ω) 수치를 실시간 수집 및 반올림 정리합니다.
-- **레이저 각인 연동 (LMC1):** 최종 종합 합격(`PASS`) 건에 한하여 2D QR 코드 및 제품 시퀀스 코드를 레이저로 마킹합니다.
+- **레이저 각인 연동 (LMC1):** 최종 종합 합격(`PASS`) 건에 한하여 QR 코드 및 제품 시퀀스 코드를 레이저로 마킹합니다.
- **규격 파라미터 조율:** 생산 제품 사양 변경 시 최소/최대 임계치(전압, 전류, 저항 범위)를 간편하게 변경 적용할 수 있습니다.
---
@@ -74,9 +74,12 @@ graph TD
- 사전 전원 체크 ➔ S/N 수집 및 보정 ➔ 주 테스트 전원 인가 ➔ **전압(V), 저항(kΩ/Ω), 전류(mA)** 순차 실측 ➔ 테스트 전원 안전 차단까지 총 15단계 과정이 백그라운드에서 비동기로 자동 진행됩니다.
2. **합부 판정 (PASS / FAIL):**
- 수집된 3개 측정값이 모두 지정된 최소/최대 규격치 이내인 경우 대형 판정 카드가 초록색 **`PASS`**로 변경됩니다. 단 하나라도 벗어나면 빨간색 **`FAIL`**이 출력됩니다.
-3. **데이터베이스 적재 및 레이저 각인 (LMC1):**
+3. **데이터베이스 적재 및 레이저 각인 (LMC1) & QR 검증:**
- 측정값은 소수점 규격에 맞춰 DB에 안전하게 보관됩니다.
- - **PASS 합격품에 한하여** LMC1 레이저 각인기를 통해 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다. (FAIL 제품은 마킹을 스킵하여 제품을 보호합니다.)
+ - **PASS 합격품에 한하여** LMC1 레이저 각인기를 통해 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다. (FAIL 제품은 마킹을 스킵하여 제품을 보호합니다.)
+ - **QR 스캔 교차 검증**: 각인 완료 후 QR 검증 팝업이 활성화됩니다.
+ - **검증 성공**: 스캔값이 일치하면 팝업이 지연 없이 **즉시 자동으로 닫히며** 최종 `OK` 상태로 DB 저장이 완료됩니다.
+ - **검증 실패/취소**: 팝업 확인 후 닫히면 메인 화면 대형 판정 카드가 즉시 **`NG`** (빨간색)로 전환되고, `"마킹 완료 / 각인 QR 검증 NG"` 및 `"상태: 검사 불합격 (각인 QR 검증 실패)"` 설명이 표출되며 DB에 `NG` 결과로 적재됩니다.
---