diff --git a/.vs/marking_gui.slnx/FileContentIndex/a26e37eb-5b6f-417f-8426-dfd11cf0e629.vsidx b/.vs/marking_gui.slnx/FileContentIndex/a26e37eb-5b6f-417f-8426-dfd11cf0e629.vsidx
deleted file mode 100644
index f298f97..0000000
Binary files a/.vs/marking_gui.slnx/FileContentIndex/a26e37eb-5b6f-417f-8426-dfd11cf0e629.vsidx and /dev/null differ
diff --git a/.vs/marking_gui.slnx/v18/.suo b/.vs/marking_gui.slnx/v18/.suo
index 7a14d40..ff9e500 100644
Binary files a/.vs/marking_gui.slnx/v18/.suo and b/.vs/marking_gui.slnx/v18/.suo differ
diff --git a/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json b/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json
index e9a678d..165fc2d 100644
--- a/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json
+++ b/.vs/marking_gui.slnx/v18/DocumentLayout.backup.json
@@ -29,7 +29,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\MainWindow.xaml",
"RelativeToolTip": "marking_gui\\MainWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
- "WhenOpened": "2026-06-16T05:11:43.746Z",
+ "WhenOpened": "2026-07-03T01:49:59.337Z",
"EditorCaption": ""
}
]
diff --git a/.vs/marking_gui.slnx/v18/DocumentLayout.json b/.vs/marking_gui.slnx/v18/DocumentLayout.json
index e9a678d..165fc2d 100644
--- a/.vs/marking_gui.slnx/v18/DocumentLayout.json
+++ b/.vs/marking_gui.slnx/v18/DocumentLayout.json
@@ -29,7 +29,7 @@
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\MainWindow.xaml",
"RelativeToolTip": "marking_gui\\MainWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
- "WhenOpened": "2026-06-16T05:11:43.746Z",
+ "WhenOpened": "2026-07-03T01:49:59.337Z",
"EditorCaption": ""
}
]
diff --git a/docs/developer_manual.md b/docs/developer_manual.md
new file mode 100644
index 0000000..c668be9
--- /dev/null
+++ b/docs/developer_manual.md
@@ -0,0 +1,361 @@
+# Performance Inspect & Laser Marking System - 개발자 메뉴얼
+
+본 문서는 **양산형 테스트 기기 모니터링 시스템**의 유지보수, 릴리즈, 시스템 통합 및 디버깅을 위한 개발자용 상세 기술 사양서입니다. 시스템 설계 정보, 하드웨어 연동 규격, 그리고 제어 시퀀스를 포함하고 있습니다.
+
+---
+
+## 1. 시스템 아키텍처 및 요구사항
+
+### 1.1 기술 스택
+- **프레임워크**: .NET Framework 4.7.2 (WPF)
+- **UI 패턴**: MVVM (MainWindow, SettingsWindow, StartupLoginWindow 등)
+- **데이터베이스**: Microsoft SQL Server 2012 이상 (ADO.NET 연동)
+- **장비 연동**:
+ - LMC1 레이저 각인 컨트롤러 (C++ Wrapped DLL `MarkEzdWrapper.dll` 로드)
+ - Keysight DMM & PSU (TCP/IP 기반 SCPI 통신)
+ - 4251 센서 판독 보드 (시리얼 RS-232 기반 고유 프로토콜)
+
+### 1.2 개발 환경 구동 요구사항
+- Visual Studio 2022 이상
+- MSBuild 엔진 (XAML 컴파일 지원)
+- MS-SQL Server Express 또는 로컬 DB 인프라
+- EZCAD 2 소프트웨어 및 LMC USB 각인 카드 드라이버
+
+---
+
+## 2. 설정 파일 (INI) 상세 사양
+
+실행 파일 경로에 위치하는 INI 설정 파일들의 각 항목별 역할 및 타입 정의입니다.
+
+### 2.1 Setting.ini
+계측 장비 접속 주소, SCPI 제어 명령어 매개변수 및 레이저 각인 도안 태그 매핑(`[Tags]`) 설정을 포함합니다.
+
+```ini
+; =================================================================
+; 성능 검사 모니터링 시스템 - 레이저 마킹기 연동 설정 파일
+; =================================================================
+
+[Settings]
+; 마킹 제어 라이브러리 및 드라이버(EzCad) 폴더 경로
+EzdFolderPath=c:\EzCad
+
+; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출)
+test=false
+
+
+[Tags]
+; 도안(.ezd) 파일 내의 텍스트 오브젝트 치환용 태그명 매핑 리스트
+; (도안에 정의된 오브젝트명과 동일하게 설정해야 정상 작동합니다)
+
+; 일련번호 (Serial Number) 치환 태그
+SerialTag=SERIAL
+
+; 작업 년월일 (Lot No) 치환 태그
+LotNoTag=YYMMDD
+
+; 제품 모델명 (Product Name) 치환 태그
+ProductTag=PRODUCT
+
+; 2D QR 코드 데이터 치환 태그
+QrCodeTag=QRCODETAG
+
+; 검사 라인 코드 치환 태그
+LineTag=LCODE
+
+; 작업 조 (Shift) 치환 태그
+ShiftTag=BAN
+
+
+; =================================================================
+; 계측기 연동 설정 (SCPI / LAN)
+; =================================================================
+
+[Equipment]
+; 계측기 통신 타임아웃 (ms)
+Timeout=5000
+
+; 측정 안정화 대기 시간 (ms) — Power Supply ON 후 이 시간만큼 대기 후 읽음
+SettleMilliseconds=2000
+
+; 검사 전 보드 전원 공급 커맨드 (세미콜론으로 구분)
+PreBoardCommand=VOLT 5, (@2);OUTP ON, (@2)
+PreBoardDelayMilliseconds=2000
+
+; 로그아웃 시 전원 차단 커맨드
+LogoutCommand=OUTP OFF, (@1)
+
+
+[DMM]
+; Digital Multimeter — 전압 및 저항 측정 전용 (예시 IP: 192.168.1.100)
+VoltageConnection=LAN
+VoltageHost=192.168.1.100
+VoltagePort=5025
+VoltageIdnMatch=34465A
+VoltageBaudRate=115200
+VoltageTerminator=LF
+VoltageSetupCommand=
+VoltageReadCommand=MEAS:VOLT:DC?
+ResistanceReadCommand=MEAS:RES?
+
+
+[CurrentDMM]
+; Digital Multimeter — 전류 측정 전용 (예시 IP: 192.168.1.102 - 현재 제어 시퀀스에서 미사용)
+CurrentDMMConnection=LAN
+CurrentDMMHost=192.168.1.102
+CurrentDMMPort=5025
+CurrentDMMIdnMatch=34465A
+CurrentDMMBaudRate=115200
+CurrentDMMTerminator=LF
+CurrentDMMReadCommand=MEAS:CURR:DC?
+
+
+[PowerSupply]
+; Power Supply — 출력 제어 및 전류 측정 (예시 IP: 192.168.1.101)
+CurrentConnection=LAN
+CurrentHost=192.168.1.101
+CurrentPort=5025
+CurrentIdnMatch=E36233A
+CurrentBaudRate=9600
+CurrentTerminator=LF
+CurrentSetupCommand=OUTP ON, (@1)
+CurrentCleanupCommand=OUTP OFF, (@1)
+CurrentReadCommand=MEAS:CURR? (@1)
+
+
+[Limits]
+; 최종 계측 합/불 판정 규격 임계치 (Min: 하한치, Max: 상한치)
+; UI [시험 설정] 메뉴에서 변경 시 자동 저장 및 갱신됩니다.
+MinVoltage=4.0
+MaxVoltage=6.0
+
+MinCurrent=8.0
+MaxCurrent=10.0
+
+MinResistance=0.4
+MaxResistance=0.8
+```
+
+### 2.2 Database.ini
+생산 이력 조회 및 적재 목적의 DB 연결 설정입니다.
+
+```ini
+[Database]
+# SQL 데이터베이스 접속 주소 및 계정 정보 (예시 주소)
+Db_Server=192.168.1.200
+Db_Name=MarkingDb
+Db_User=db_user
+Db_Password=db_password
+
+# 로컬 테스트 및 DB 점검 시 우회용 플래그
+OfflineMode=False
+
+
+[Login]
+# 작업자 로그인 검증 시 호출할 저장 프로시저명
+Procedure=dbo.CheckOperator
+
+# 작업 공정 식별 이름 (저장 프로시저 매개변수 바인딩용)
+ProcessName=Marking
+
+# 오프라인 로그인 미리보기 활성화 여부
+OfflinePreview=false
+```
+
+### 2.3 Sensor.ini
+4251 센서 통신 보드 시리얼 통신을 위한 포트 매핑 파일입니다.
+
+```ini
+[Sensor]
+# 시리얼 COM 포트 번호
+Port=COM3
+
+# 통신 속도
+BaudRate=115200
+
+# 보드 내부 멀티 채널 스위칭 인덱스 (1 또는 2)
+Channel=1
+```
+
+---
+
+## 3. 데이터베이스 스키마 및 연동 규격
+
+### 3.1 테이블 구성 정보
+- **`[dbo].[Users]` (작업자 계정 정보 테이블)**
+ - `OperatorId` (VARCHAR(50), UNIQUE): 작업자 로그인 ID
+ - `Password` (VARCHAR(50)): 비밀번호
+ - `Name` (VARCHAR(50)): 작업자 성명
+ - `IsActive` (BIT): 활성 상태 여부
+
+- **`[dbo].[Marking]` (최종 성능 계측값 및 마킹 결과 저장 테이블)**
+ - `IC_SN` (VARCHAR(50), PRIMARY KEY): 센서 일련번호
+ - `Marking_QR` (VARCHAR(100), UNIQUE): 고유 QR 코드 문자열 (yyMMdd + 일련번호)
+ - `Maker` (VARCHAR(50)), `Model` (VARCHAR(50)): 제품 사양 정보
+ - `Variant_1`, `Variant_2` (VARCHAR(50))
+ - `Operator` (VARCHAR(50)): 담당 작업자 ID
+ - `Production_Date` (DATETIME): 생산 일자 및 계측 시간
+ - `Line` (VARCHAR(10)): 생산 라인 (최대 10자 제한, 초과 시 코드 내에서 절삭)
+ - `Lot_No` (VARCHAR(8)): 로트 번호 (최대 8자 제한, 초과 시 코드 내에서 절삭)
+ - `Jig_No` (VARCHAR(10)): 지그 번호
+ - `PT_Vol_1` (DECIMAL(18,2)), `PT_Current_1` (DECIMAL(18,2)), `PT_Resistance_1` (DECIMAL(18,3)): 실측 전압, 전류, 저항값
+ - `Result` (VARCHAR(10)): 최종 종합 판정 (`PASS` 또는 `FAIL`)
+ - `MarkedSerial` (VARCHAR(50)): 최종 마킹된 시퀀스 코드
+
+### 3.2 이전 공정 결과 교차 검증 (Subquery)
+검사 개시 시점, 제품의 S/N(`IC_SN`)을 기반으로 선행 공정이 완료되었는지 조회합니다.
+```sql
+SELECT
+ (SELECT Result FROM Housing_Assembly WHERE IC_SN = @IC_SN) AS HousingResult,
+ (SELECT Final_Result FROM Cal_EOL_Result WHERE IC_SN = @IC_SN) AS CalResult,
+ (SELECT Result FROM EOL WHERE IC_SN = @IC_SN) AS EolResult
+```
+- **판정 조건**: 조회 결과 3개 항목이 모두 `PASS` 또는 `OK` 상태이어야 최종 검사 기동이 허용됩니다.
+
+### 3.3 로그인 저장 프로시저 (`[dbo].[CheckOperator]`)
+- **프로시저 스펙**:
+ ```sql
+ CREATE PROCEDURE [dbo].[CheckOperator]
+ @Operator VARCHAR(50),
+ @Password VARCHAR(50),
+ @ProcessName VARCHAR(50) = NULL
+ AS
+ BEGIN
+ SELECT COUNT(*)
+ FROM Users
+ WHERE OperatorId = @Operator
+ AND Password = @Password
+ AND IsActive = 1
+ END
+ ```
+- **호출 매핑 코드**:
+ ADO.NET `SqlCommand` 객체를 사용하여 `CommandType.StoredProcedure` 형식으로 호출하며 리턴값이 `1`일 경우 검증 처리합니다.
+
+---
+
+## 4. 하드웨어 제어 프로토콜 및 SCPI 사양
+
+### 4.1 4251 센서 통신 보드 시리얼 패킷 규격
+센서 일련번호를 추출하고 보드를 제어하기 위해 RS-232 시리얼(115200bps)을 통해 다음 패킷 명령을 전송합니다.
+
+1. **보드 대기 및 리셋 명령**: `"x00o"`
+2. **보드 채널 결속 명령**: `"x00c_00{channel}101:owt28006727ea97c7801"`
+ - `{channel}`은 `Sensor.ini`에서 파싱한 값 (예: `1` 또는 `2`)
+3. **고유 ID(IC_SN) 읽기 명령**: `"x00c_00{channel}101:ow2800326003e"`
+ - 송신 후 보드 응답 버퍼에서 16진수 일련번호 데이터(16자)를 추출합니다.
+4. **보정 기본값 전송**: `"x00c_00{channel}001:owt28006727ea97c7801"`
+
+### 4.2 계측 장비 제어 SCPI 명령어 구성 및 시퀀스
+모든 TCP/IP 계측기는 포트 **5025**를 통해 SCPI 표준 명령으로 조작됩니다.
+
+1. **장비 연결 및 아이덴티티 검증 (Handshake)**:
+ - 각 계측기 접속 후 `*IDN?` 명령을 전송하여 응답 텍스트에 `Setting.ini`에 지정된 장비 모델명(예: `34465A`, `E36233A`)이 수록되어 있는지 확인합니다.
+2. **테스트 주전원(CH1) 인가**:
+ - `OUTP ON, (@1)`
+ - 전원 공급 후 신호 안정을 위해 `SettleMilliseconds`(기본 2000ms, 즉 2초)만큼 대기합니다.
+3. **측정 데이터 수집**:
+ - **전압(V)**: 전압 DMM(예: 192.168.1.100)에 `MEAS:VOLT:DC?`를 질의하여 획득.
+ - **저항(Ω)**: 저항 DMM(예: 192.168.1.100)에 `MEAS:RES?`를 질의하여 획득. (획득값은 kΩ 단위 조정을 위해 1000으로 나눔)
+ - **전류(A)**: 파워서플라이(예: 192.168.1.101)의 CH1에 `MEAS:CURR? (@1)`을 질의하여 획득. (획득값은 mA 단위 조정을 위해 1000을 곱함)
+ - *주의: 전류 측정 전용 DMM(예: 192.168.1.102) 장비는 설정 파일에 로드되나 실제 측정 시퀀스 상에서는 사용되지 않습니다.*
+4. **주전원(CH1) 차단**:
+ - `OUTP OFF, (@1)`
+ - 계측 도중 예외가 발생하더라도 `finally` 구문 내에서 차단 명령을 전송합니다.
+
+---
+
+## 5. 레이저 마킹기 연동 사양 (LMC1 API)
+
+C++로 개발된 레이저 각인기 드라이버 DLL인 `MarkEzdWrapper.dll`을 P/Invoke를 통해 호출합니다.
+
+### 5.1 연동 시 핵심 규칙
+1. **세션 핸들 (`hHandle`) 관리**:
+ - 프로그램 시작 시 `InitMarkEzd()` 함수를 호출하여 세션 포인터(`IntPtr`)를 받아 전역 변수로 관리합니다.
+ - 각인 관련 API 함수 호출 시 이 `IntPtr` 핸들을 첫 번째 매개변수로 전달해야 합니다.
+2. **유니코드 마샬링 (`CharSet.Unicode`)**:
+ - C++ DLL 단의 모든 문자열 인자가 `const wchar_t*` (유니코드)를 사용하므로, DllImport 선언의 문자셋 속성은 `CharSet = CharSet.Unicode`로 설정해야 정상 작동합니다.
+3. **HWND 부모 창 핸들 지연 초기화**:
+ - `Init` 함수는 부모 WPF 윈도우의 핸들(`IntPtr`)을 요구합니다.
+ - Window 생성자 시점에는 핸들이 미생성 상태이므로, 반드시 **`Dispatcher.BeginInvoke(..., DispatcherPriority.Loaded)`**를 활용하여 메인 윈도우 로드가 완료된 후에 핸들을 추출해 초기화해야 합니다.
+
+### 5.2 DLL 가져오기 (P/Invoke) 선언 명세
+```csharp
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "InitMarkEzd", CallingConvention = CallingConvention.Cdecl)]
+private static extern IntPtr InitMarkEzd();
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int Init(IntPtr hHandle, string strEzCadPath, IntPtr hOwenWnd);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "LoadEzdFile", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int LoadEzdFile(IntPtr hHandle, string strEzCadFileName);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "ChangeTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int ChangeTextByName(IntPtr hHandle, string strTextName, string strTextNew);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Mark", CallingConvention = CallingConvention.Cdecl)]
+private static extern int Mark(IntPtr hHandle);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Close", CallingConvention = CallingConvention.Cdecl)]
+private static extern int Close(IntPtr hHandle);
+```
+
+---
+
+## 6. 데모용 시뮬레이션 격리 및 릴리즈 배포 가이드
+
+### 6.1 데모 모드 격리 코드 해제 방법
+상용 빌드 릴리즈 시, 데이터베이스에 실제 로그인하지 않고 통과하는 데모 우회 로직을 소스 코드 단에서 제거해야 합니다.
+
+1. **[LoginServicesStub.cs](file:///C:/Users/COMPUTER1/Desktop/mobi/marking_gui/marking_gui/Services/LoginServicesStub.cs)** 파일을 엽니다.
+2. `ExistsAsync` 메서드 안의 `catch (Exception ex)` 구문 내부에 위치한 아래의 **DEMO MODE 주석 배너** 영역 코드를 삭제하거나 주석 처리하여 릴리즈 빌드를 만드십시오.
+
+```csharp
+// ==================== [DEMO MODE START] ====================
+// 데모 시뮬레이션용 프로시저 성공 연출 코드
+// 상용 릴리즈 시 이 블록은 완전히 제거(삭제)할 수 있습니다.
+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] ====================
+```
+
+### 6.2 릴리즈 게시 및 단일 설치 파일(MarkingSetup.exe) 자동 생성 가이드
+
+본 프로젝트는 Visual Studio 게시 프로필 및 WiX Toolset 커스텀 타깃이 적용되어 있어, CLI 게시 명령 한 번으로 단일 실행 설치 파일(`MarkingSetup.exe`)을 자동으로 컴파일 및 생성합니다.
+
+- **권장 CLI 게시 명령어 (Terminal)**:
+ ```powershell
+ dotnet publish marking_gui/marking_gui.csproj -c Release -r win-x86 --self-contained
+ ```
+- **자동 빌드 및 패키징 동작 순서**:
+ 1. 프로젝트 릴리즈 바이너리 및 의존 라이브러리 일체 자동 수집
+ 2. `C:\marking\libs\` 경로로 모든 런타임 DLL 격리 구성
+ 3. WiX Toolset(`wix build`)이 가동하여 MSI 패키지 및 단일 EXE 부트스트랩 번들 컴파일
+ 4. 최종 설치 파일 `MarkingSetup.exe`가 `C:\marking\` 및 `bin\Release\net9.0-windows\win-x86\publish\` 폴더로 자동 생성 이관
+- **배포 및 전달 대상 파일**:
+ - `MarkingSetup.exe` (단일 파일만 현장에 전달하여 1-Click 설치 진행)
+
+### 6.3 배포 설치 디렉터리(`C:\marking\`) 파일 구조 및 런타임 생성 폴더 사양
+
+`MarkingSetup.exe`로 인스톨된 후 `C:\marking\` 아래에 배치되는 파일 및 런타임 가동 중 생성되는 디렉터리 명세입니다.
+
+```
+C:\marking\
+├── marking_gui.exe # 메인 WPF 실행 바이너리
+├── marking_gui.dll # 핵심 애플리케이션 어셈블리
+├── marking_gui.deps.json # 종속성 매니페스트
+├── marking_gui.runtimeconfig.json # .NET 9.0 런타임 환경 구성
+├── Setting.ini # 마킹기 경로, 태그 매핑, 계측기 IP, SCPI 전원, [Limits] 판정 임계치 종합 설정
+├── Database.ini # MS-SQL 접속 계정 및 오프라인 모드(OfflineMode) 설정
+├── Sensor.ini # 4251 센서 통신 보드 시리얼 포트(COM), BaudRate, 채널 설정
+├── libs\ # C++ LMC 드라이버 라이브러리 격리 폴더
+│ ├── MarkEzdWrapper.dll # C# - C++ LMC P/Invoke 래퍼
+│ ├── DataMgr.dll # EZCAD 데이터 매니저
+│ ├── MarkEzd.dll # EZCAD 커널 마킹 라이브러리
+│ └── haspms32.dll # 동글/라이선스 보안 라이브러리 일체
+├── logs\ # 런타임 가동 중 일별 자동 생성되는 시스템/통신/마킹 이력 로그
+│ └── 2026-07-23.log # 텍스트 형태의 일자별 로그 파일
+└── publish_backup\ # 게시 및 버전 업데이트 시 기존 파일 자동 백업 보관 폴더
+```
diff --git a/docs/knowledge_base.md b/docs/knowledge_base.md
index b332383..e3e1cd0 100644
--- a/docs/knowledge_base.md
+++ b/docs/knowledge_base.md
@@ -69,20 +69,35 @@
```
4. 삭제 후 빌드를 다시 수행하면, DB 통신 실패 시 로그인이 우회되지 않고 정상적으로 "로그인 DB 확인 실패" 경고가 발생하게 됩니다.
-### 3.2. 빌드 실행 방법
-본 프로젝트는 레거시 .NET Framework 4.7.2 타깃의 WPF 애플리케이션으로, XAML 마크업 빌드가 정상 동작하기 위해서는 Visual Studio 환경의 MSBuild 엔진이 필요합니다.
-- **MSBuild 권장 경로:** `C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\MSBuild.exe` (설치 버전에 따라 Insiders, Community, Professional 등으로 달라질 수 있음)
-- **빌드 명령어 (터미널):**
+### 3.2. 릴리즈 게시 및 단일 설치 파일 패키징 가이드
+본 프로젝트는 **.NET 9.0 (WPF)** 타깃의 32비트(x86) 자립형 애플리케이션으로, 프로젝트 파일([marking_gui.csproj](file:///C:/Users/COMPUTER1/Desktop/mobi/marking_gui/marking_gui/marking_gui.csproj))에 WiX Toolset 단일 통합 설치 마법사 컴파일 타깃이 통합되어 있습니다.
+
+- **권장 CLI 게시 명령어 (터미널):**
```powershell
- & "C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\MSBuild.exe" /t:rebuild
+ dotnet publish marking_gui/marking_gui.csproj -c Release -r win-x86 --self-contained
```
-- **출력 경로:** 빌드 완수 후 `marking_gui\bin\Debug\` 또는 `Release\` 폴더에 실행 바이너리 및 `Database.ini` 파일이 함께 생성됩니다.
+- **빌드 및 패키징 동작 메커니즘:**
+ 1. `.NET 9.0 런타임`이 포함된 자립형(Self-Contained) 싱글 실행 바이너리를 빌드합니다.
+ 2. 계측 및 각인기 드라이버 DLL들을 `C:\marking\libs\` 디렉터리로 격리 수집합니다.
+ 3. `wix build`가 백그라운드에서 동작하여 `Product.wxs` 및 `Bundle.wxs`를 단일 실행 설치 파일(`MarkingSetup.exe`)로 원스톱 패키징합니다.
+- **출력 경로 및 최종 결과물:**
+ - `C:\marking\MarkingSetup.exe` 및 `marking_gui\bin\Release\net9.0-windows\win-x86\publish\MarkingSetup.exe`
+ - 현장 라인에는 단일 파일 `MarkingSetup.exe`만 배포하여 1-Click 설치를 진행할 수 있습니다.
+
+### 3.3. 설치 경로(`C:\marking\`) 내 파일 구성 및 런타임 생성 폴더
+- `marking_gui.exe` (메인 실행 파일) 및 .NET 9.0 런타임 DLL
+- `Setting.ini` (장비 IP, 마킹 태그, SCPI 커맨드, `[Limits]` 규격 임계치)
+- `Database.ini` (DB 접속 주소 및 `OfflineMode=true` 설정)
+- `Sensor.ini` (4251 센서 통신 보드 COM 포트/속도/채널)
+- `libs/` (LMC 레이저 각인 드라이버 격리 디렉터리: `MarkEzdWrapper.dll` 등)
+- `logs/` (런타임 가동 시 일자별 자동 생성되는 시스템/각인/통신 이력 로그 폴더)
+- `publish_backup/` (빌드 배포 시 이전 릴리즈 자동 백업 폴더)
---
## 4. 데이터베이스 구성 가이드 (Database Configuration)
-본 시스템은 **Microsoft SQL Server 2012** 이상의 버전에 완전히 호환되도록 구성되었습니다. 데이터베이스 구성 시 [docs/database_setup.sql](file:///C:/Users/COMPUTER1/Desktop/mobi/marking_gui/docs/database_setup.sql) 스크립트를 활용할 수 있습니다.
+본 시스템은 **Microsoft SQL Server 2012** 이상의 버전에 완전히 호환되도록 구성되었습니다. 데이터베이스 구성 시 [sql/database_setup.sql](file:///C:/Users/COMPUTER1/Desktop/mobi/marking_gui/sql/database_setup.sql) 스크립트를 활용할 수 있습니다.
### 4.1. 데이터베이스 및 스키마 구조
- **데이터베이스명:** `MarkingDb`
@@ -103,3 +118,43 @@
- `@ProcessName` (VARCHAR(50)): 호출 프로세스명 (선택사항, 기본값 NULL)
- **리턴:** 매칭되는 활성 사용자가 존재할 시 `1`, 존재하지 않을 시 `0`을 스칼라 결과로 반환합니다.
+---
+
+## 5. 레이저 각인기 연동 규격 및 구현 사양 (Laser Marking Device Integration)
+
+본 프로그램은 **LMC1 레이저 각인 컨트롤러 보드** 제어를 위해 C++ DLL Wrapper인 `MarkEzdWrapper.dll`을 활용하며, 하드웨어와의 메모리 정합성 및 실기 작동 안전성을 완벽히 조율한 최종 기술 사양은 다음과 같습니다.
+
+### 5.1. 핵심 연동 규칙 및 메커니즘
+1. **IntPtr 기반의 기기 세션 핸들(hHandle) 관리**:
+ - `InitMarkEzd()` 함수를 기동하여 생성되는 장치 관리 메모리 세션 포인터(`IntPtr`)를 받아 전역 변수에 저장합니다.
+ - 이후 모든 연동 API(`Init`, `LoadEzdFile`, `ChangeTextByName`, `Mark`, `Close`)의 첫 번째 매개변수로 이 `IntPtr` 핸들을 인계하여 호출해야 합니다. 임의의 정수 값(`0` 등)이나 `ref int` 를 대입하여 호출하면 메모리 위반 크래시(`AccessViolationException`)가 발생합니다.
+2. **유니코드(`CharSet.Unicode`) 마샬링 준수**:
+ - C++ DLL 단의 모든 문자열 인자는 `const wchar_t*` (유니코드)를 사용합니다.
+ - 따라서 모든 DllImport 선언의 문자셋 속성은 반드시 **`CharSet = CharSet.Unicode`** 여야 합니다. `Ansi`로 지정할 시 경로명 및 파일명이 한자 깨진 글자로 읽혀 `EZCAD.CFG failed` 에러를 냅니다.
+3. **WPF 윈도우 핸들(HWND) 지연 초기화**:
+ - `Init` API 호출 시 실제 유효한 부모 윈도우 핸들(`IntPtr windowHandle`)이 필요합니다.
+ - MainWindow의 생성자 시점에는 핸들이 미생성 상태(`IntPtr.Zero`)이므로 즉시 초기화하면 안 됩니다.
+ - 반드시 WPF **`Dispatcher.BeginInvoke(..., DispatcherPriority.Loaded)`** 지연 큐를 활용하여 메인 윈도우 로드가 완료된 후에 유효한 창 핸들을 추출해 이니셜라이즈 해야 합니다.
+
+### 5.2. P/Invoke C# 선언 명세
+```csharp
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "InitMarkEzd", CallingConvention = CallingConvention.Cdecl)]
+private static extern IntPtr InitMarkEzd();
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int Init(IntPtr hHandle, string strEzCadPath, IntPtr hOwenWnd);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "LoadEzdFile", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int LoadEzdFile(IntPtr hHandle, string strEzCadFileName);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "ChangeTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int ChangeTextByName(IntPtr hHandle, string strTextName, string strTextNew);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Mark", CallingConvention = CallingConvention.Cdecl)]
+private static extern int Mark(IntPtr hHandle);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Close", CallingConvention = CallingConvention.Cdecl)]
+private static extern int Close(IntPtr hHandle);
+```
+
+
diff --git a/marking_gui.Setup/Bundle.wxs b/marking_gui.Setup/Bundle.wxs
new file mode 100644
index 0000000..e4077c1
--- /dev/null
+++ b/marking_gui.Setup/Bundle.wxs
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/marking_gui.Setup/Product.wxs b/marking_gui.Setup/Product.wxs
new file mode 100644
index 0000000..f1bedbb
--- /dev/null
+++ b/marking_gui.Setup/Product.wxs
@@ -0,0 +1,147 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/marking_gui/.agents/skills/laser_marking/SKILL.md b/marking_gui/.agents/skills/laser_marking/SKILL.md
new file mode 100644
index 0000000..db58849
--- /dev/null
+++ b/marking_gui/.agents/skills/laser_marking/SKILL.md
@@ -0,0 +1,87 @@
+---
+name: laser_marking_integration
+description: Guidelines and P/Invoke signatures for integrating the LMC laser marking machine via MarkEzdWrapper.dll without crashes.
+---
+
+# LMC 레이저 각인기 연동 규격 및 에이전트 행동 규칙
+
+이 규칙은 `MarkEzdWrapper.dll` 파일과 WPF 모니터링 시스템 간의 레이저 각인(마킹) 장치 연동 설계 사양을 고정합니다. **이 사양은 어떠한 경우에도 임의로 단순 값 형식의 int 핸들이나 Ansi 마샬링으로 되돌려서는 안 되며, 반드시 이 명세를 유지해야 합니다.**
+
+---
+
+## 1. 연동 핵심 원칙 (Core Rules)
+
+1. **IntPtr 기반의 기기 세션 핸들 관리**:
+ - `InitMarkEzd()` 함수를 최초 실행하여 생성되는 장치 관리 메모리 세션 포인터(`IntPtr`)를 받아 전역 변수에 저장합니다.
+ - 이후 모든 연동 API(`Init`, `LoadEzdFile`, `ChangeTextByName`, `Mark`, `Close`)의 첫 번째 매개변수로 이 `IntPtr` 핸들을 인계하여 호출해야 합니다. 임의의 정수 값(`0` 등)이나 `ref int` 를 대입하여 호출하면 메모리 위반 크래시(`AccessViolationException`)가 발생합니다.
+
+2. **유니코드(`CharSet.Unicode`) 마샬링 준수**:
+ - C++ DLL 단의 모든 문자열 인자는 `const wchar_t*` (유니코드)를 사용합니다.
+ - 따라서 모든 DllImport 선언의 문자셋 속성은 반드시 **`CharSet = CharSet.Unicode`** 여야 합니다. `Ansi`로 지정할 시 경로명 및 파일명이 한자 깨진 글자로 읽혀 `EZCAD.CFG failed` 에러를 냅니다.
+
+3. **WPF 윈도우 핸들(HWND) 지연 초기화**:
+ - `Init` API 호출 시 실제 유효한 부모 윈도우 핸들(`IntPtr windowHandle`)이 필요합니다.
+ - MainWindow의 생성자 시점에는 핸들이 미생성 상태(`IntPtr.Zero`)이므로 즉시 초기화하면 안 됩니다.
+ - 반드시 WPF **`Dispatcher.BeginInvoke(..., DispatcherPriority.Loaded)`** 지연 큐를 활용하여 메인 윈도우 로드가 완료된 후에 유효한 창 핸들을 추출해 이니셜라이즈 해야 합니다.
+
+---
+
+## 2. P/Invoke 표준 선언부 (Standard C# Declarations)
+
+```csharp
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "InitMarkEzd", CallingConvention = CallingConvention.Cdecl)]
+private static extern IntPtr InitMarkEzd();
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int Init(IntPtr hHandle, string strEzCadPath, IntPtr hOwenWnd);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "LoadEzdFile", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int LoadEzdFile(IntPtr hHandle, string strEzCadFileName);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "ChangeTextByName", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+private static extern int ChangeTextByName(IntPtr hHandle, string strTextName, string strTextNew);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Mark", CallingConvention = CallingConvention.Cdecl)]
+private static extern int Mark(IntPtr hHandle);
+
+[DllImport("MarkEzdWrapper.dll", EntryPoint = "Close", CallingConvention = CallingConvention.Cdecl)]
+private static extern int Close(IntPtr hHandle);
+```
+
+---
+
+## 3. 핵심 비즈니스 흐름 코드 (Core Implementation Patterns)
+
+### 3.1. 장치 초기화 흐름
+```csharp
+// 1. 선행 엔진 초기화 및 포인터 획득
+IntPtr markingHandle = InitMarkEzd();
+if (markingHandle == IntPtr.Zero)
+{
+ // 보드 또는 동글 락키 인식 실패 예외 처리
+ return false;
+}
+
+// 2. WPF 메인 윈도우 실제 창 핸들 획득 (Loaded 시점)
+IntPtr windowHandle = new System.Windows.Interop.WindowInteropHelper(this).Handle;
+
+// 3. 컨트롤러 장비 초기화 수행
+int initResult = Init(markingHandle, ezcadFolderPath, windowHandle);
+if (initResult == 0)
+{
+ // 연결 완료 상태 전환
+}
+```
+
+### 3.2. 공정 각인 요청 흐름
+```csharp
+// 1. 도안 로드
+int loadResult = LoadEzdFile(markingHandle, absoluteEzdPath);
+
+// 2. 도안 내 변수 태그 치환 (Marking.ini 매핑 기준)
+ChangeTextByName(markingHandle, "SERIAL", request.Serial);
+ChangeTextByName(markingHandle, "YYMMDD", request.LotNo);
+
+// 3. 각인 기계 동작 기동
+int markResult = Mark(markingHandle);
+```
diff --git a/marking_gui/App.xaml.cs b/marking_gui/App.xaml.cs
index 8231783..36ae1aa 100644
--- a/marking_gui/App.xaml.cs
+++ b/marking_gui/App.xaml.cs
@@ -13,12 +13,72 @@ namespace marking_gui
///
public partial class App : Application
{
+ [System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
+ private static extern bool SetDllDirectory(string lpPathName);
+
protected override void OnStartup(StartupEventArgs e)
{
+ // 바로가기 등으로 실행 시 작업 디렉토리가 꼬이는 현상을 방지하기 위해 작업 디렉토리를 실행 파일 폴더로 강제 동기화
+ try
+ {
+ System.IO.Directory.SetCurrentDirectory(AppDomain.CurrentDomain.BaseDirectory);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("작업 디렉토리 초기화 중 에러: " + ex.Message);
+ }
+
+ // 전역 예외 처리 이벤트 핸들러 등록
+ AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
+ this.DispatcherUnhandledException += App_DispatcherUnhandledException;
+ TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
+
+ Services.LoggerService.Info("=================== [프로그램 시작] ===================");
+
+ try
+ {
+ // DLL 탐색 경로에 실행 디렉토리 하위의 libs 폴더 추가
+ string libsDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "libs");
+ if (System.IO.Directory.Exists(libsDir))
+ {
+ SetDllDirectory(libsDir);
+ Services.LoggerService.Info("libs 폴더 경로 추가 완료: " + libsDir);
+ }
+ }
+ catch (Exception ex)
+ {
+ Services.LoggerService.Error("SetDllDirectory 설정 중 예외 발생", ex);
+ }
+
base.OnStartup(e);
var mainWindow = new MainWindow();
mainWindow.Show();
}
+
+ protected override void OnExit(ExitEventArgs e)
+ {
+ Services.LoggerService.Info("=================== [프로그램 정상 종료] ===================");
+ base.OnExit(e);
+ }
+
+ private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
+ {
+ var ex = e.ExceptionObject as Exception;
+ Services.LoggerService.Error("[비정상 종료] 처리되지 않은 도메인 예외 (UnhandledException) 발생", ex);
+ }
+
+ private void App_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
+ {
+ Services.LoggerService.Error("[비정상 종료] 처리되지 않은 UI 스레드 예외 (DispatcherUnhandledException) 발생", e.Exception);
+ // 메인 루프를 유지하기 위해 예외를 처리된 것으로 설정
+ e.Handled = true;
+ }
+
+ private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
+ {
+ Services.LoggerService.Error("[작동 오류] 처리되지 않은 비동기 태스크 예외 (UnobservedTaskException) 발생", e.Exception);
+ e.SetObserved();
+ }
}
}
diff --git a/marking_gui/LoginOptions.json b/marking_gui/LoginOptions.json
new file mode 100644
index 0000000..15c0037
--- /dev/null
+++ b/marking_gui/LoginOptions.json
@@ -0,0 +1,23 @@
+{
+ "loginAccounts": [
+ {
+ "id": "su",
+ "password": "su"
+ }
+ ],
+ "makers": [
+ "AMO",
+ "ABC"
+ ],
+ "models": [
+ "MODEL-1"
+ ],
+ "variant1Values": [],
+ "variant2Values": [],
+ "lineValues": [
+ "LINE-1"
+ ],
+ "jigNoValues": [
+ "JIG-1"
+ ]
+}
diff --git a/marking_gui/MainWindow.xaml b/marking_gui/MainWindow.xaml
index 3316cc2..861f332 100644
--- a/marking_gui/MainWindow.xaml
+++ b/marking_gui/MainWindow.xaml
@@ -6,6 +6,7 @@
xmlns:local="clr-namespace:marking_gui"
mc:Ignorable="d"
Title="양산형 테스트 기기 모니터링 시스템" Height="700" Width="900"
+ Icon="pack://application:,,,/Marking.ico"
MinWidth="900" MinHeight="700" WindowState="Maximized" WindowStyle="None"
WindowStartupLocation="CenterScreen" Background="#F3F4F6"
ContentRendered="Window_ContentRendered">
@@ -111,6 +112,34 @@
+
+
+
@@ -132,35 +161,39 @@
-
+
+
+
-
-
-
+
+
+
-
+
+
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -179,7 +212,8 @@
-
+
+
@@ -194,13 +228,49 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -211,7 +281,7 @@
-
+
@@ -228,8 +298,8 @@
-
-
+
+
@@ -239,8 +309,8 @@
-
-
+
+
@@ -250,8 +320,8 @@
-
-
+
+
@@ -276,81 +346,198 @@
-
+
-
+
-
+
+
+
-
+
-
+
-
+
-
+
+
+
-
+
-
+
-
+
-
+
+
+
-
-
+
+
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
@@ -359,13 +546,32 @@
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/marking_gui/MainWindow.xaml.cs b/marking_gui/MainWindow.xaml.cs
index 915baa3..3877946 100644
--- a/marking_gui/MainWindow.xaml.cs
+++ b/marking_gui/MainWindow.xaml.cs
@@ -1,8 +1,10 @@
using System;
+using System.Threading;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
+using System.Threading.Tasks;
using marking_gui.Models;
using marking_gui.Services;
@@ -13,27 +15,294 @@ namespace marking_gui
///
public partial class MainWindow : Window
{
- private DispatcherTimer _measurementTimer;
private DispatcherTimer _clockTimer;
- private int _timerTickCount = 0;
// 비즈니스 로직 및 가상 하드웨어/DB 서비스 분리
- private readonly MssqlDatabaseService _database = new MssqlDatabaseService();
- private readonly MockMeasurer _measurer = new MockMeasurer();
+ private readonly DatabaseService _database = new DatabaseService();
private readonly MemoryReaderService _memoryReader = new MemoryReaderService();
private MeasurementLimits _limits = new MeasurementLimits();
private Housing.Login.StartupLoginWindowResult _loginInfo;
+ // 마킹 기계 연동 서비스 필드 추가
+ private readonly MarkingConfigService _markingConfig = new MarkingConfigService();
+ private readonly IMarkingService _markingService;
+ private string _currentEzdFile = string.Empty;
+ private bool _isMarkingDeviceInitialized = false;
+
+ // DB 재연결 자동 감시 필드
+ private CancellationTokenSource _dbWatcherCts;
+ private bool _dbWasOffline = false;
+
+
+
+ // 계측기 연동 서비스 (DMM 전압 + Power Supply 전류)
+ private EquipmentMeasurementService _equipment;
+
+ // 검사 시작 / 종료 시간 추적
+ private DateTime _testStartTime = DateTime.MinValue;
+ private DateTime _testEndTime = DateTime.MinValue;
+
+ // 현재 검사 중인 제품 정보 (EOL PCB 바코드 포함)
+ private Product _currentProduct = null;
+
+ // 4251 센서 하드웨어 연동 필드 (leak_test_project 포팅)
+ private SerialProvider _boardSerial;
+ private Board4251Service _boardService;
+ private Board4251SensorService _sensorReader;
+
public MainWindow()
{
InitializeComponent();
_loginInfo = new Housing.Login.StartupLoginWindowResult { Operator = "---", Model = "---", LineNo = "---" };
-
- InitializeTimer();
+
+ // 실서버용 마킹 연동 서비스 활성화 (데모 바인딩 제거)
+ _markingService = new EzdMarkingService();
+
+ // 계측기 연동 서비스 초기화 (Setting.ini 의 [DMM] / [PowerSupply] 섹션 사용)
+ string settingIniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
+ _equipment = new EquipmentMeasurementService(settingIniPath);
+
InitializeClock();
ResetUI();
InitializeLimits();
DisplayLoginInfo();
+
+ InitializeSensorReader();
+ ApplyTestModeSettings();
+ InitializeEzdFileSelection();
+
+ // 윈도우 종료 시 전원 차단 연동
+ this.Closed += MainWindow_Closed;
+
+ // DB 재연결 자동 감시 시작 (OfflineMode가 아닌 경우에만)
+ if (!_database.OfflineMode)
+ {
+ _dbWatcherCts = new CancellationTokenSource();
+ StartDbReconnectWatcher(_dbWatcherCts.Token);
+ }
+ }
+
+ private void ApplyTestModeSettings()
+ {
+ bool isTestMode = _markingConfig.IsTestMode;
+ var visibility = isTestMode ? Visibility.Visible : Visibility.Collapsed;
+
+ if (btnBypassStart != null)
+ {
+ btnBypassStart.Visibility = visibility;
+ }
+ if (btnTestMarking != null)
+ {
+ btnTestMarking.Visibility = visibility;
+ }
+ }
+
+ private async void InitializeMarkingDevice()
+ {
+ if (_isMarkingDeviceInitialized)
+ {
+ return;
+ }
+
+ try
+ {
+ // 오프라인 모드인 경우 하드웨어 LMC 초기화 과정 우회
+ if (_database.OfflineMode)
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "오프라인 작동 중";
+ if (txtMarkingStatus != null) txtMarkingStatus.ToolTip = "Database.ini 설정에 의해 시뮬레이션 모드로 작동 중입니다.";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(107, 114, 128)); // Gray
+ LoggerService.Info("오프라인 모드 감지: 마킹 기계 LMC 초기화 우회");
+ return;
+ }
+
+ // MainWindow의 실제 Win32 윈도우 핸들(HWND) 획득
+ IntPtr windowHandle = new System.Windows.Interop.WindowInteropHelper(this).Handle;
+
+ // Setting.ini 또는 EzCad 설정 폴더 경로 지정하여 디바이스 초기화 (1회 시도)
+ string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
+ bool initResult = await System.Threading.Tasks.Task.Run(() =>
+ {
+ LoggerService.Info("마킹 디바이스 LMC 초기화 시도...");
+ return _markingService.Initialize(iniPath, windowHandle);
+ });
+
+ if (initResult)
+ {
+ _isMarkingDeviceInitialized = true;
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "연결 완료";
+ if (txtMarkingStatus != null) txtMarkingStatus.ToolTip = "실서버 LMC 컨트롤러와 정상 통신 중입니다.";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
+ LoggerService.Info("마킹 디바이스 초기화 성공 (연결 완료)");
+ }
+ else
+ {
+ string details = (_markingService as EzdMarkingService)?.LastInitErrorMessage ?? "알 수 없는 연결 실패";
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "연결 실패 (기기 미확인)";
+ if (txtMarkingStatus != null) txtMarkingStatus.ToolTip = "장비 초기화 실패 사유:\n" + details;
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange
+ LoggerService.Warn("마킹 디바이스 초기화 실패: " + details);
+ }
+ }
+ catch (Exception ex)
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "연동 에러";
+ if (txtMarkingStatus != null) txtMarkingStatus.ToolTip = "마킹 초기화 중 예외 발생: " + ex.Message;
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
+ LoggerService.Error("마킹 디바이스 초기화 중 예외 발생", ex);
+ }
+ }
+
+ // 4251 센서 초기화 (COM3 115200 기본 / Sensor.ini 포트 연동)
+ private void InitializeSensorReader()
+ {
+ try
+ {
+ // 기존 활성화된 센서 리소스 안전 해제 및 이벤트 바인딩 언바인드
+ try
+ {
+ if (_sensorReader != null)
+ {
+ _sensorReader.ProgressMessage -= OnSensorProgressMessage;
+ _sensorReader.ErrorMessage -= OnSensorErrorMessage;
+ _sensorReader.Dispose();
+ }
+ }
+ catch { }
+ _sensorReader = null;
+
+ try
+ {
+ _boardService?.Dispose();
+ }
+ catch { }
+ _boardService = null;
+
+ try
+ {
+ _boardSerial?.Dispose();
+ }
+ catch { }
+ _boardSerial = null;
+
+ string portName = "COM3";
+ int baudRate = 115200;
+ int channelVal = 1;
+
+ string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sensor.ini");
+ if (System.IO.File.Exists(iniPath))
+ {
+ var lines = System.IO.File.ReadAllLines(iniPath);
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+ if (trimmed.StartsWith("Port", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2) portName = parts[1].Trim();
+ }
+ else if (trimmed.StartsWith("BaudRate", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2) int.TryParse(parts[1].Trim(), out baudRate);
+ }
+ else if (trimmed.StartsWith("Channel", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2) int.TryParse(parts[1].Trim(), out channelVal);
+ }
+ }
+ }
+
+ if (channelVal < 1 || channelVal > 2) channelVal = 1;
+
+ _boardSerial = new SerialProvider(portName, baudRate);
+ _boardService = new Board4251Service(_boardSerial);
+ _sensorReader = new Board4251SensorService(_boardService, channelVal - 1); // 설정된 채널 index로 연결
+
+ // 실시간 수집 상태 및 에러 메시지 UI 바인딩 연동 (메서드 매핑으로 메모리 누수 차단)
+ _sensorReader.ProgressMessage += OnSensorProgressMessage;
+ _sensorReader.ErrorMessage += OnSensorErrorMessage;
+
+ bool connected = _sensorReader.Connect();
+ LoggerService.Info(string.Format("4251 센서 {0} ({1}bps, 채널 CH{2}) 연결 시도 결과: {3}", portName, baudRate, channelVal, connected));
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error("4251 센서 장치 초기화 중 예외 발생", ex);
+ }
+ }
+
+ private void OnSensorProgressMessage(object sender, string msg)
+ {
+ Dispatcher.Invoke(() => {
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = msg;
+ txtProgressStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(59, 130, 246)); // Blue
+ }
+ });
+ }
+
+ private void OnSensorErrorMessage(object sender, string err)
+ {
+ Dispatcher.Invoke(() => {
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "센서 읽기 실패";
+ txtProgressStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)); // Red
+ }
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = err;
+ }
+ if (borderErrorBanner != null)
+ {
+ borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ }
+ if (borderNormalStatus != null)
+ {
+ borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+ }
+ });
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ try
+ {
+ try
+ {
+ _dbWatcherCts?.Cancel();
+ _dbWatcherCts?.Dispose();
+ }
+ catch { }
+
+ _markingService?.Shutdown();
+
+ if (_sensorReader != null)
+ {
+ _sensorReader.ProgressMessage -= OnSensorProgressMessage;
+ _sensorReader.ErrorMessage -= OnSensorErrorMessage;
+ _sensorReader.Dispose();
+ }
+
+ _boardService?.Dispose();
+ _boardSerial?.Dispose();
+
+ // 마킹기 P/Invoke 핸들 정리용 Dispose
+ _markingService?.Dispose();
+
+ // 계측기 TCP 연결 유지 소켓 Dispose
+ _equipment?.Dispose();
+
+ System.Diagnostics.Debug.WriteLine("[Sensor] 4251 센서 및 장비 리소스 해제 완료.");
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[Sensor] 리소스 해제 중 오류: " + ex.Message);
+ }
+ base.OnClosed(e);
}
private void DisplayLoginInfo()
@@ -42,23 +311,26 @@ namespace marking_gui
{
txtOperatorInfo.Text = _loginInfo.Operator;
}
- if (txtModelInfo != null)
+
+ if (txtModelInfo != null && !string.IsNullOrEmpty(_loginInfo.Model))
{
- txtModelInfo.Text = string.IsNullOrEmpty(_loginInfo.Model) ? "N/A" : _loginInfo.Model;
+ txtModelInfo.Text = _loginInfo.Model;
}
+
if (txtLineInfo != null)
{
txtLineInfo.Text = string.IsNullOrEmpty(_loginInfo.LineNo) ? "N/A" : _loginInfo.LineNo;
}
- }
- private void InitializeTimer()
- {
- _measurementTimer = new DispatcherTimer();
- _measurementTimer.Interval = TimeSpan.FromMilliseconds(200); // 0.2초마다 갱신 (시뮬레이션 효과 연출)
- _measurementTimer.Tick += MeasurementTimer_Tick;
+ if (txtCurrentLotNo != null)
+ {
+ txtCurrentLotNo.Text = string.IsNullOrEmpty(_loginInfo.LotNo) ? "N/A" : _loginInfo.LotNo;
+ }
+
+ RefreshNextSequence();
}
+
private void InitializeClock()
{
_clockTimer = new DispatcherTimer();
@@ -72,14 +344,16 @@ namespace marking_gui
private void InitializeLimits()
{
+ string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
+ _limits = MeasurementLimits.LoadFromIni(iniPath);
UpdateRangeGuides();
}
private void UpdateRangeGuides()
{
txtVoltageRangeGuide.Text = string.Format("기준범위: {0:F1}V ~ {1:F1}V", _limits.MinVoltage, _limits.MaxVoltage);
- txtCurrentRangeGuide.Text = string.Format("기준범위: {0:F1}A ~ {1:F1}A", _limits.MinCurrent, _limits.MaxCurrent);
- txtResistanceRangeGuide.Text = string.Format("기준범위: {0:F1}Ω ~ {1:F1}Ω", _limits.MinResistance, _limits.MaxResistance);
+ 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);
}
private void btnOpenSettings_Click(object sender, RoutedEventArgs e)
@@ -90,11 +364,43 @@ namespace marking_gui
if (settings.ShowDialog() == true)
{
_limits = settings.Limits;
+ string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
+ _limits.SaveToIni(iniPath);
UpdateRangeGuides();
- MessageBox.Show("기준 범위 설정이 적용되었습니다.", "설정 완료", MessageBoxButton.OK, MessageBoxImage.Information);
+ MessageBox.Show("기준 범위 설정이 저장 및 적용되었습니다.", "설정 완료", MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+ }
+
+ private void btnOpenPortSettings_Click(object sender, RoutedEventArgs e)
+ {
+ PortSettingsWindow portSettings = new PortSettingsWindow();
+ portSettings.Owner = this;
+
+ if (portSettings.ShowDialog() == true)
+ {
+ // 포트 설정 변경 및 저장 성공 시 센서 재연결 처리
+ InitializeSensorReader();
+ MessageBox.Show("포트 설정이 적용되었습니다. 센서 장치를 재연결합니다.", "설정 완료", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
+ // ── 버튼 상태 헬퍼 ────────────────────────────────────────────────
+ /// 검사 진행 중 → 버튼 비활성화 + 텍스트 변경
+ private void SetStartButtonBusy()
+ {
+ btnStart.IsEnabled = false;
+ btnStart.Content = "⏳ 검사 진행 중...";
+ if (btnBypassStart != null) btnBypassStart.IsEnabled = false;
+ }
+
+ /// 검사 완료/취소 → 버튼 활성화 + 텍스트 원복
+ private void SetStartButtonReady()
+ {
+ btnStart.IsEnabled = true;
+ btnStart.Content = "▶ 검사 시작 (START)";
+ if (btnBypassStart != null) btnBypassStart.IsEnabled = true;
+ }
+
// UI 초기화 상태
private void ResetUI()
{
@@ -114,156 +420,478 @@ namespace marking_gui
txtCurrent.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
txtResistance.Text = "-.--";
txtResistance.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
+ if (borderVoltage != null)
+ {
+ borderVoltage.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E5E7EB"));
+ borderVoltage.BorderThickness = new Thickness(1);
+ }
+ if (borderCurrent != null)
+ {
+ borderCurrent.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E5E7EB"));
+ borderCurrent.BorderThickness = new Thickness(1);
+ }
+ if (borderResistance != null)
+ {
+ borderResistance.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#E5E7EB"));
+ borderResistance.BorderThickness = new Thickness(1);
+ }
+
+ txtFinalResult.Text = "대기 중";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175)); // Gray text for card
+ if (badgeFinalResult != null)
+ {
+ badgeFinalResult.Background = System.Windows.Media.Brushes.White;
+ badgeFinalResult.BorderBrush = new SolidColorBrush(Color.FromRgb(229, 231, 235)); // Gray
+ }
+ if (borderVerdictCard != null)
+ {
+ borderVerdictCard.Background = System.Windows.Media.Brushes.White;
+ borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(229, 231, 235)); // Gray
+ borderVerdictCard.BorderThickness = new Thickness(1);
+ }
+ if (borderFinalResult != null)
+ {
+ borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
+ }
+ txtFinalSerial.Text = "---";
+ if (txtPcbBarcode != null) txtPcbBarcode.Text = "---";
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = string.Empty;
+ }
+ if (borderErrorBanner != null)
+ {
+ borderErrorBanner.Visibility = System.Windows.Visibility.Collapsed;
+ }
+ if (borderNormalStatus != null)
+ {
+ borderNormalStatus.Visibility = System.Windows.Visibility.Visible;
+ }
+ if (txtMarkedSerial != null)
+ {
+ txtMarkedSerial.Text = "---";
+ }
- txtFinalResult.Text = "대 기 중";
- txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
- borderFinalResult.Background = new SolidColorBrush(Color.FromRgb(229, 231, 235)); // Light Gray
- txtFinalSerial.Text = "S/N: ---";
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "대기 상태";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(75, 85, 99)); // Gray-600 text for white card
+ }
- btnStart.IsEnabled = true;
+ if (txtTestStartTime != null)
+ {
+ txtTestStartTime.Text = "시작 시간: --:--:--";
+ }
+ if (txtTestEndTime != null)
+ {
+ txtTestEndTime.Text = "종료 시간: --:--:--";
+ }
+
+ SetStartButtonReady();
+ if (btnTestMarking != null)
+ {
+ btnTestMarking.IsEnabled = true;
+ }
}
// 스타트 버튼 클릭
- private void btnStart_Click(object sender, RoutedEventArgs e)
+ private async void btnStart_Click(object sender, RoutedEventArgs e)
{
- // 새로운 검사 진행을 위해 UI 상태를 초기화
- ResetUI();
+ await RunInspectionSequenceAsync(bypassPrevSteps: false);
+ }
+
+ // 강제 검사 (스킵) 버튼 클릭
+ private async void btnBypassStart_Click(object sender, RoutedEventArgs e)
+ {
+ await RunInspectionSequenceAsync(bypassPrevSteps: true);
+ }
+
+ private async System.Threading.Tasks.Task RunInspectionSequenceAsync(bool bypassPrevSteps)
+ {
+ // 진입하자마자 즉각 버튼을 비활성화하여 중복 클릭 원천 차단 (비동기 대기 이전에 호출 필수)
+ SetStartButtonBusy();
- // 1. 메모리에서 S/N 자동 획득
- string serial = "";
+ // 0. 각인 일련번호 사전 초과 검증 (9999 초과 시 테스트 시작 차단)
+ int checkSeq;
try
{
- serial = _memoryReader.ReadSerialNumber();
+ checkSeq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo);
}
- catch (Exception ex)
+ catch (Exception seqEx)
{
- MessageBox.Show("메모리에서 S/N을 읽는 중 오류가 발생했습니다: " + ex.Message, "오류", MessageBoxButton.OK, MessageBoxImage.Error);
+ LoggerService.Error("[순번 조회 실패] DB에서 각인 순번을 가져오지 못했습니다.", seqEx);
+ string errMsg = $"각인 순번 DB 조회 실패: {seqEx.Message}";
+ if (txtSystemStatus != null)
+ {
+ txtSystemStatus.Text = errMsg;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ if (txtErrorMessage != null) txtErrorMessage.Text = errMsg;
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Collapsed;
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "검사 시작 실패 (순번 조회 불가)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ SetStartButtonReady();
return;
}
- if (string.IsNullOrEmpty(serial))
+ if (checkSeq > 9999)
{
- MessageBox.Show("메모리에서 유효한 S/N을 읽지 못했습니다.", "경고", MessageBoxButton.OK, MessageBoxImage.Warning);
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Collapsed;
+ if (txtErrorMessage != null) txtErrorMessage.Text = "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오.";
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "검사 시작 실패 (Lot 순번 초과)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ MessageBox.Show("각인 순번이 9999를 초과하여 더 이상 검사를 진행할 수 없습니다.\n다음 Lot No로 변경해 주십시오.", "Lot 순번 초과", MessageBoxButton.OK, MessageBoxImage.Error);
+
+ SetStartButtonReady();
return;
}
- // UI에 S/N 표시
- txtSerial.Text = serial;
- txtFinalSerial.Text = "S/N: " + serial;
+ // 검사 시작 누르자마자 이전의 측정 결과 및 상태 화면을 즉시 클리어
+ ResetUI();
- // 2. DB에서 이전 결과값 확인
- Product product = null;
- try
- {
- product = _database.GetProductBySerial(serial);
- }
- catch (Exception ex)
+ // ResetUI 내부에서 버튼이 다시 활성화(Ready)되므로 Busy 상태로 잠금 유지
+ SetStartButtonBusy();
+
+ // 검사 시작 즉시 IC_SN 입력란을 초기화 (이전 S/N이 남지 않도록)
+ txtSerial.Text = string.Empty;
+ txtFinalSerial.Text = "S/N: -";
+
+ // ── STEP 1. IC_SN 읽기 (DB 조회 전에 먼저 수행) ──────────────────
+ string serial = string.Empty; // txtSerial을 클리어했으므로 항상 센서에서 읽어옴
+
+ if (txtProgressStatus != null)
{
- MessageBox.Show(ex.Message, "DB 조회 오류", MessageBoxButton.OK, MessageBoxImage.Error);
- txtSystemStatus.Text = "상태: DB 연동 에러";
- txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
- return;
+ txtProgressStatus.Text = "제품 정보(IC_SN) 수집 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
}
- if (product != null)
+ // S/N 가 비어있는 경우, 센서 하드웨어 리더로부터 자동 수집 시도
+ if (string.IsNullOrEmpty(serial))
{
- // 이전 검사 결과 바인딩 및 색상 설정
- txtPrevHousing.Text = product.HousingResult;
- txtPrevHousing.Foreground = product.HousingResult == "PASS"
- ? new SolidColorBrush(Color.FromRgb(16, 185, 129))
- : new SolidColorBrush(Color.FromRgb(239, 68, 68));
-
- txtPrevCal.Text = product.CalResult;
- txtPrevCal.Foreground = product.CalResult == "PASS"
- ? new SolidColorBrush(Color.FromRgb(16, 185, 129))
- : new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ if (_sensorReader != null)
+ {
+ txtSystemStatus.Text = "상태: 센서 ID 읽는 중...";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246));
- txtPrevEol.Text = product.EolResult;
- txtPrevEol.Foreground = product.EolResult == "PASS"
- ? new SolidColorBrush(Color.FromRgb(16, 185, 129))
- : new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ try
+ {
+ // 4251 하드웨어 보드로부터 센서 ID를 비동기로 읽어옵니다. (15초 타임아웃)
+ var sensorData = await _sensorReader.ReadSensorAsync();
+ if (sensorData != null && !string.IsNullOrEmpty(sensorData.ID))
+ {
+ serial = sensorData.ID.Trim();
+ txtSerial.Text = serial;
+ LoggerService.Info($"센서 리더로부터 자동 수집 성공 (S/N: {serial})");
+ }
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error("센서 ID 자동 수집 중 예외 발생", ex);
+ txtSystemStatus.Text = "센서 수집 오류: " + ex.Message;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ }
- // 3. 불합격 존재 시 즉시 팝업 창 출력 및 진행 불가 표시 후 중단
- if (!product.IsPreviousStepsPassed)
+ // 센서로도 읽지 못했고 빈값인 경우 처리
+ if (string.IsNullOrEmpty(serial))
{
- string failedStep = string.Empty;
- if (product.HousingResult == "FAIL") failedStep = "Housing 검사";
- else if (product.CalResult == "FAIL") failedStep = "Calibration 검사";
- else if (product.EolResult == "FAIL") failedStep = "EOL 검사";
+ if (_database.OfflineMode)
+ {
+ // 오프라인 모드인 경우 원활한 단위 테스트를 위해 임시 S/N를 발행해 진행시킵니다.
+ serial = "OFFLINE_" + DateTime.Now.ToString("HHmmss");
+ txtSerial.Text = serial;
+ LoggerService.Info($"오프라인 자동 S/N 발급: {serial}");
+ }
+ else
+ {
+ LoggerService.Warn("제품 ID(IC_SN)를 획득하지 못해 검사를 취소했습니다.");
- txtSystemStatus.Text = "상태: 진행 불가 (이전 단계 불합격)";
- txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ // IC_SN 읽기 실패 시 버튼 원복 후 종료
+ SetStartButtonReady();
- MessageBox.Show(string.Format("이전 공정 단계({0})가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다.", failedStep), "검사 제한", MessageBoxButton.OK, MessageBoxImage.Error);
- return;
+ MessageBox.Show("장치 메모리로부터 제품 ID(IC_SN)를 읽어오지 못했습니다. 제품 안착 상태를 확인해 주십시오.", "IC_SN 로드 실패", MessageBoxButton.OK, MessageBoxImage.Warning);
+ txtSerial.Focus();
+ return;
+ }
}
+ }
- // 4. 모두 합격(PASS) 시 자동으로 최종 성능 검사(타이머) 진행
- btnStart.IsEnabled = false;
- txtSerial.IsEnabled = false;
-
- txtSystemStatus.Text = "상태: 검사 진행 중...";
+ // ── STEP 2. DB 연결 확인 (일반 검사만, 강제 검사는 BYPASS) ─────────
+ bool isDbConnected = true;
+ if (!bypassPrevSteps)
+ {
+ txtSystemStatus.Text = "상태: DB 연결 확인 중...";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "데이터베이스 연결 상태 확인 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
+ }
- txtFinalResult.Text = "측 정 중";
- txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246));
- borderFinalResult.Background = new SolidColorBrush(Color.FromRgb(219, 234, 254)); // Light Blue
-
- // 시뮬레이션 타이머 시작
- _timerTickCount = 0;
- _measurementTimer.Start();
+ isDbConnected = await System.Threading.Tasks.Task.Run(() => _database.CheckConnection());
}
- else
- {
- // 등록되지 않은 S/N일 경우, UI의 이전 결과 상태는 대기로 리셋하고 팝업 안내
- txtPrevHousing.Text = "대기";
- txtPrevHousing.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
- txtPrevCal.Text = "대기";
- txtPrevCal.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
- txtPrevEol.Text = "대기";
- txtPrevEol.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
- txtSystemStatus.Text = "상태: 미등록 제품";
+ if (!bypassPrevSteps && !isDbConnected)
+ {
+ txtSystemStatus.Text = "상태: DB 연동 에러";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "조회 에러 (DB 연동 실패)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
- MessageBox.Show("등록되지 않은 S/N입니다.\n데모 모드 시나리오가 순환됩니다. 다시 시도해 주세요.", "조회 실패", MessageBoxButton.OK, MessageBoxImage.Information);
+ // 에러 발생 및 검사 중단 시 버튼 상태 원복
+ SetStartButtonReady();
- // 대기 상태로 복구
- txtSystemStatus.Text = "상태: 대기 중";
- txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
+ MessageBox.Show("데이터베이스 서버에 연결할 수 없습니다. DB 연결 상태를 확인해 주십시오.", "DB 연결 오류", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
}
- }
- // 타이머 틱 이벤트 (측정값 변화 시뮬레이션)
- private void MeasurementTimer_Tick(object sender, EventArgs e)
- {
- _timerTickCount++;
+ // ── STEP 3. UI 상태 갱신 및 이전 공정 조회 준비 ─────────────────────
+ // 새로운 검사 진행을 위해 UI 상태를 초기화
+ ResetUI();
- if (_timerTickCount < 10) // 약 2초 동안 측정값 흔들림 효과 연출
+ // UI 상태 복원 및 진행 개시 알림
+ txtSerial.Text = serial;
+ txtFinalSerial.Text = "S/N: " + serial;
+
+ if (txtProgressStatus != null)
{
- double tempVoltage, tempCurrent, tempResistance;
- _measurer.GenerateNoiseValues(_limits, out tempVoltage, out tempCurrent, out tempResistance);
-
- txtVoltage.Text = tempVoltage.ToString("F2");
- txtCurrent.Text = tempCurrent.ToString("F2");
- txtResistance.Text = tempResistance.ToString("F2");
+ txtProgressStatus.Text = "이전 공정 검사 결과 조회 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
}
- else // 최종 측정 완료 및 판정
- {
- _measurementTimer.Stop();
-
- double finalVoltage, finalCurrent, finalResistance;
- _measurer.GenerateFinalValues(_limits, out finalVoltage, out finalCurrent, out finalResistance);
+
+ txtSystemStatus.Text = "상태: 제품 정보 조회 중...";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
+
+ LoggerService.Info(string.Format("----------------- [검사 진행 시작{0}] S/N: {1} -----------------", bypassPrevSteps ? " - BYPASS" : "", serial));
- txtVoltage.Text = finalVoltage.ToString("F2");
- txtCurrent.Text = finalCurrent.ToString("F2");
- txtResistance.Text = finalResistance.ToString("F2");
+ SetStartButtonBusy();
- // 합/불 판정
- bool isVoltageOk = finalVoltage >= _limits.MinVoltage && finalVoltage <= _limits.MaxVoltage;
- bool isCurrentOk = finalCurrent >= _limits.MinCurrent && finalCurrent <= _limits.MaxCurrent;
+ // 2. DB에서 이전 결과값 비동기 확인 (강제 검사의 경우 대기/조회하지 않고 패스)
+ Product product = null;
+ if (!bypassPrevSteps)
+ {
+ try
+ {
+ product = await System.Threading.Tasks.Task.Run(() => _database.GetProductBySerial(serial));
+ _currentProduct = product; // EOL PCB 바코드 포함 저장
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error($"DB 이전 단계 결과 조회 중 예외 발생 (S/N: {serial})", ex);
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "조회 에러: " + ex.Message;
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
+ }
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = "DB 조회 오류: " + ex.Message;
+ }
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Collapsed;
+
+ MessageBox.Show(ex.Message, "DB 조회 오류", MessageBoxButton.OK, MessageBoxImage.Error);
+ txtSystemStatus.Text = "오류: " + ex.Message;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+
+ SetStartButtonReady();
+ return;
+ }
+ }
+
+ bool shouldProceed = false;
+
+ if (product != null)
+ {
+ // 이전 검사 결과 바인딩 및 색상 설정
+ txtPrevHousing.Text = product.HousingResult;
+ txtPrevHousing.Foreground = (product.HousingResult == "PASS" || product.HousingResult == "OK")
+ ? new SolidColorBrush(Color.FromRgb(16, 185, 129))
+ : new SolidColorBrush(Color.FromRgb(239, 68, 68));
+
+ txtPrevCal.Text = product.CalResult;
+ txtPrevCal.Foreground = (product.CalResult == "PASS" || product.CalResult == "OK")
+ ? new SolidColorBrush(Color.FromRgb(16, 185, 129))
+ : new SolidColorBrush(Color.FromRgb(239, 68, 68));
+
+ txtPrevEol.Text = product.EolResult;
+ txtPrevEol.Foreground = (product.EolResult == "PASS" || product.EolResult == "OK")
+ ? new SolidColorBrush(Color.FromRgb(16, 185, 129))
+ : new SolidColorBrush(Color.FromRgb(239, 68, 68));
+
+ if (txtPcbBarcode != null)
+ {
+ txtPcbBarcode.Text = string.IsNullOrWhiteSpace(product.PcbBarcode) ? "---" : product.PcbBarcode;
+ }
+
+ if (product.IsPreviousStepsPassed)
+ {
+ shouldProceed = true;
+ }
+ else
+ {
+ if (bypassPrevSteps)
+ {
+ LoggerService.Warn(string.Format("[강제 검사 (BYPASS)] 이전 공정에 불합격이 있으나 스킵 설정이므로 진행합니다. (Housing:{0}, Cal:{1}, Eol:{2})", product.HousingResult, product.CalResult, product.EolResult));
+ shouldProceed = true;
+ }
+ else
+ {
+ string failedStep = string.Empty;
+ if (product.HousingResult == "FAIL" || product.HousingResult == "NG") failedStep = "Housing 검사";
+ else if (product.CalResult == "FAIL" || product.CalResult == "NG") failedStep = "Calibration 검사";
+ else if (product.EolResult == "FAIL" || product.EolResult == "NG") failedStep = "EOL 검사";
+
+ txtSystemStatus.Text = "상태: 진행 불가 (이전 단계 불합격)";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "진행 불가 (이전 공정 불합격)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text for white card
+ }
+
+ MessageBox.Show(string.Format("이전 공정 단계({0})가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다.", failedStep), "검사 제한", MessageBoxButton.OK, MessageBoxImage.Error);
+ return;
+ }
+ }
+ }
+ else
+ {
+ if (bypassPrevSteps)
+ {
+ LoggerService.Warn(string.Format("[강제 검사 (BYPASS)] 미등록 제품(S/N: {0})이지만 스킵 설정이므로 계속 진행합니다.", serial));
+
+ txtPrevHousing.Text = "스킵";
+ txtPrevHousing.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange
+ txtPrevCal.Text = "스킵";
+ txtPrevCal.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
+ txtPrevEol.Text = "스킵";
+ txtPrevEol.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
+
+ shouldProceed = true;
+ }
+ else
+ {
+ // 등록되지 않은 S/N일 경우, UI의 이전 결과 상태는 대기로 리셋하고 팝업 안내
+ txtPrevHousing.Text = "대기";
+ txtPrevHousing.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
+ txtPrevCal.Text = "대기";
+ txtPrevCal.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
+ txtPrevEol.Text = "대기";
+ txtPrevEol.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
+
+ txtSystemStatus.Text = "상태: 미등록 제품";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "조회 실패 (미등록 제품)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
+ }
+
+ MessageBox.Show("이전 공정(Housing/Calibration/EOL) 이력이 조회되지 않는 S/N입니다. 제품 번호 또는 공정 단계를 확인해 주십시오.", "조회 실패", MessageBoxButton.OK, MessageBoxImage.Warning);
+
+ // 대기 상태로 복구
+ txtSystemStatus.Text = "상태: 대기 중";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175));
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "대기 상태";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(75, 85, 99)); // Gray-600
+ }
+ return;
+ }
+ }
+
+ if (shouldProceed)
+ {
+ // 4. 모두 합격(PASS) 시 자동으로 최종 성능 검사(타이머) 진행
+ SetStartButtonBusy();
+ txtSerial.IsEnabled = false;
+
+ txtSystemStatus.Text = "상태: 검사 진행 중...";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
+
+ txtFinalResult.Text = "측정 중";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue text
+ if (borderVerdictCard != null)
+ {
+ borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(239, 246, 255)); // Light Blue background
+ borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue border
+ borderVerdictCard.BorderThickness = new Thickness(2);
+ }
+ if (badgeFinalResult != null)
+ {
+ badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white
+ badgeFinalResult.BorderBrush = new SolidColorBrush(Color.FromRgb(229, 231, 235)); // Gray
+ }
+ borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "측정 계측기 데이터 수집 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue text
+ }
+
+ // 시작 시간 기록 및 UI 출력
+ _testStartTime = DateTime.Now;
+ txtTestStartTime.Text = "시작 시간: " + _testStartTime.ToString("yyyy-MM-dd HH:mm:ss");
+ txtTestEndTime.Text = "종료 시간: --:--:--";
+
+ // 비동기로 하드웨어로부터 계측값 획득 (통신 장애 예외 방어)
+ double finalVoltage = 0.0;
+ double finalCurrent = 0.0;
+ double finalResistance = 0.0;
+
+ try
+ {
+ var measurement = await ReadMeasurementFromDeviceAsync();
+ finalVoltage = measurement.Voltage;
+ finalCurrent = measurement.Current;
+ finalResistance = measurement.Resistance;
+ }
+ catch (Exception measEx)
+ {
+ LoggerService.Error("계측기 데이터 수집 중 예외 발생", measEx);
+ txtSystemStatus.Text = "상태: 계측 통신 에러";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "계측 실패: " + measEx.Message;
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ if (txtErrorMessage != null) txtErrorMessage.Text = "계측 통신 오류: " + measEx.Message;
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Collapsed;
+
+ MessageBox.Show($"계측기 수신 장애 발생: {measEx.Message}\n\n계측기 네트워크(LAN)선 및 통신 설정을 확인하십시오.", "계측 통신 오류", MessageBoxButton.OK, MessageBoxImage.Error);
+
+ SetStartButtonReady();
+ txtSerial.IsEnabled = true;
+ return;
+ }
+
+ txtVoltage.Text = finalVoltage.ToString("F2");
+ txtCurrent.Text = finalCurrent.ToString("F2");
+ txtResistance.Text = finalResistance.ToString("00.000");
+
+ // 합/불 판정
+ bool isVoltageOk = finalVoltage >= _limits.MinVoltage && finalVoltage <= _limits.MaxVoltage;
+ bool isCurrentOk = finalCurrent >= _limits.MinCurrent && finalCurrent <= _limits.MaxCurrent;
bool isResistanceOk = finalResistance >= _limits.MinResistance && finalResistance <= _limits.MaxResistance;
bool isFinalPass = isVoltageOk && isCurrentOk && isResistanceOk;
@@ -272,30 +900,313 @@ namespace marking_gui
txtCurrent.Foreground = isCurrentOk ? new SolidColorBrush(Color.FromRgb(31, 41, 55)) : new SolidColorBrush(Color.FromRgb(239, 68, 68));
txtResistance.Foreground = isResistanceOk ? new SolidColorBrush(Color.FromRgb(31, 41, 55)) : new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ if (borderVoltage != null)
+ {
+ borderVoltage.BorderBrush = isVoltageOk ? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E")) : new SolidColorBrush((Color)ColorConverter.ConvertFromString("#EF4444"));
+ borderVoltage.BorderThickness = new Thickness(3);
+ }
+ if (borderCurrent != null)
+ {
+ borderCurrent.BorderBrush = isCurrentOk ? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E")) : new SolidColorBrush((Color)ColorConverter.ConvertFromString("#EF4444"));
+ borderCurrent.BorderThickness = new Thickness(3);
+ }
+ if (borderResistance != null)
+ {
+ borderResistance.BorderBrush = isResistanceOk ? new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E")) : new SolidColorBrush((Color)ColorConverter.ConvertFromString("#EF4444"));
+ borderResistance.BorderThickness = new Thickness(3);
+ }
+
if (isFinalPass)
{
txtSystemStatus.Text = "상태: 검사 완료 (합격)";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
- txtFinalResult.Text = "PASS";
- txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
- borderFinalResult.Background = new SolidColorBrush(Color.FromRgb(209, 250, 229)); // Light Green
+ txtFinalResult.Text = "OK";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green text for white card
+ if (borderVerdictCard != null)
+ {
+ borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(240, 253, 250)); // Light Green background
+ borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green border
+ borderVerdictCard.BorderThickness = new Thickness(3);
+ }
+ if (badgeFinalResult != null)
+ {
+ badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white
+ }
+ borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "제품 합격 - 레이저 각인 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange text
+ }
}
else
{
txtSystemStatus.Text = "상태: 검사 완료 (불합격)";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
- txtFinalResult.Text = "FAIL";
- txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
- borderFinalResult.Background = new SolidColorBrush(Color.FromRgb(254, 226, 226)); // Light Red
+ txtFinalResult.Text = "NG";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text for white card
+ if (borderVerdictCard != null)
+ {
+ borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(254, 242, 242)); // Light Red background
+ borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red border
+ borderVerdictCard.BorderThickness = new Thickness(3);
+ }
+ if (badgeFinalResult != null)
+ {
+ badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white
+ }
+ borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
+
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "스킵 (불합격)";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(107, 114, 128)); // Gray
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "검사 불합격 (각인 생략)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
+ }
}
- // 데이터베이스에 최종 검사 결과 저장 (소수점 2자리 반올림 포맷 반영 및 공정 정보 전달)
- try
+ // 합격한 경우 마킹을 비동기로 실행하여 UI 얼어붙음 예방
+ if (isFinalPass)
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 중...";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange
+
+ _ = System.Threading.Tasks.Task.Run(async () =>
+ {
+ try
+ {
+ // 1. 각인 일련번호 조회 및 생성 (YYMMDDLL + 4자리 시퀀스)
+ int seq;
+ try
+ {
+ seq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo);
+ }
+ catch (Exception seqEx)
+ {
+ LoggerService.Error("[순번 조회 실패] 마킹 직전 DB 순번 조회 실패", seqEx);
+ Dispatcher.Invoke(() =>
+ {
+ string errMsg = $"각인 순번 DB 조회 실패: {seqEx.Message}";
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "순번 조회 실패";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ if (txtSystemStatus != null)
+ {
+ txtSystemStatus.Text = errMsg;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ if (txtErrorMessage != null) txtErrorMessage.Text = errMsg;
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "마킹 실패 (순번 조회 불가)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ MessageBox.Show($"각인 순번을 DB에서 가져오지 못했습니다.\n\n{seqEx.Message}\n\nDB 연결 상태를 확인하십시오.", "순번 조회 실패", MessageBoxButton.OK, MessageBoxImage.Error);
+ });
+ return;
+ }
+
+ string yymmdd = DateTime.Now.ToString("yyMMdd");
+ string lotNo = string.IsNullOrWhiteSpace(_loginInfo.LotNo) ? "00" : (_loginInfo.LotNo.Trim().Length < 2 ? _loginInfo.LotNo.Trim().PadLeft(2, '0') : _loginInfo.LotNo.Trim());
+ string markedSerial = string.Format("{0}{1}{2:D4}", yymmdd, 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
+ {
+ Serial = markedSerial,
+ IcSerial = serial,
+ LotNo = lotNo,
+ ProductNo = _loginInfo.Model,
+ DrawingNo = drawingNo,
+ HkmcNo = hkmcNo,
+ EzdFileName = _currentEzdFile,
+ LineCode = _loginInfo.LineNo,
+ ShiftCode = "A", // 기본 1조
+ QrString = qrString
+ };
+
+ MarkingResult markingRes;
+ if (_database.OfflineMode)
+ {
+ // 오프라인 모드 시뮬레이션
+ await System.Threading.Tasks.Task.Delay(500);
+ markingRes = new MarkingResult
+ {
+ Success = true,
+ ErrorCode = 0,
+ ErrorMessage = "오프라인 모드 각인 완료 모사",
+ MarkedAt = DateTime.Now
+ };
+ }
+ else
+ {
+ markingRes = _markingService.RequestMarking(req);
+ }
+
+ // UI 스레드로 안전하게 컴백
+ Dispatcher.Invoke(() =>
+ {
+ if (txtMarkedSerial != null)
+ {
+ txtMarkedSerial.Text = markedSerial;
+ }
+
+ bool finalMarkingPass = markingRes.Success;
+
+ if (finalMarkingPass)
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 성공";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
+ LoggerService.Info($"[마킹 성공] S/N: {serial}, 각인번호: {markedSerial}");
+ }
+ else
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 실패 (" + markingRes.ErrorCode + ")";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
+ LoggerService.Error($"[마킹 실패] S/N: {serial}, 각인번호: {markedSerial}, Code: {markingRes.ErrorCode}, Message: {markingRes.ErrorMessage}");
+
+ // 마킹 실패 시에도 계측 검사는 통과했으므로 "최종 합격(마킹 실패)"으로 표출
+ txtFinalResult.Text = "PASS";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange
+ txtSystemStatus.Text = string.Format("최종 합격 (마킹 실패) [{0}]: {1}", markingRes.ErrorCode, markingRes.ErrorMessage);
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
+
+ if (borderVerdictCard != null)
+ {
+ borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(254, 242, 242)); // Light Red
+ borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+
+ MessageBox.Show(
+ string.Format("레이저 각인 작업 전송 실패\n\n에러 코드: {0}\n에러 메시지: {1}\n\n하드웨어 통신선 연결, LMC 카드 전원, 혹은 설정 경로를 확인하십시오.",
+ markingRes.ErrorCode, markingRes.ErrorMessage),
+ "각인 실패", MessageBoxButton.OK, MessageBoxImage.Warning);
+
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = string.Format("각인 에러 [{0}]: {1}", markingRes.ErrorCode, markingRes.ErrorMessage);
+ }
+ if (borderErrorBanner != null)
+ {
+ borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ }
+ if (borderNormalStatus != null)
+ {
+ borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+ }
+ }
+
+ // DB 저장 및 마무리 (마킹 성공 시에만 최종 OK로 저장)
+ SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, finalMarkingPass, finalMarkingPass ? "OK" : "NG", markedSerial);
+ });
+ }
+ catch (Exception threadEx)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 에러";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
+ LoggerService.Error($"[마킹 스레드 오류] S/N: {serial}", threadEx);
+
+ txtFinalResult.Text = "PASS";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
+ txtSystemStatus.Text = "최종 합격 (마킹 예외): " + threadEx.Message;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
+
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = "각인 예외: " + threadEx.Message;
+ }
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+
+ MessageBox.Show("마킹 비동기 스레드 실행 중 치명적인 오류가 발생했습니다:\n" + threadEx.Message + "\n" + threadEx.StackTrace, "각인 오류", MessageBoxButton.OK, MessageBoxImage.Error);
+ SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", "ERR_SERIAL");
+ });
+ }
+ });
+ }
+ else
+ {
+ // 불합격인 경우 즉시 DB 저장 및 복구
+ SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, isFinalPass, "NG", "");
+ }
+ }
+ }
+
+ private async Task<(double Voltage, double Current, double Resistance)> ReadMeasurementFromDeviceAsync()
+ {
+ try
+ {
+ // EquipmentMeasurementService 를 통해 실시간 계측값을 획득하는 즉시 GUI에 반영
+ var (voltage, current, resistance) = await _equipment.MeasureAsync((v, c, r) =>
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (v.HasValue)
+ {
+ txtVoltage.Text = v.Value.ToString("F2");
+ }
+ if (r.HasValue)
+ {
+ txtResistance.Text = r.Value.ToString("0.000");
+ }
+ if (c.HasValue)
+ {
+ txtCurrent.Text = c.Value.ToString("F2");
+ }
+ });
+ });
+
+ return (voltage, current, resistance);
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error("[계측기] 측정 중 오류 발생", ex);
+ // 장비 오류 시 UI에 오류 메시지 표시 후 기본값 반환
+ Dispatcher.Invoke(() =>
+ {
+ txtSystemStatus.Text = "계측기 오류: " + ex.Message;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = "계측기 오류: " + ex.Message;
+ if (borderErrorBanner != null)
+ borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ }
+ });
+ return (0.00, 0.00, 0.00);
+ }
+ }
+
+ private async void SaveAndFinalize(string serial, double voltage, double current, double resistance, bool isFinalPass, string result, string markedSerial)
+ {
+ // 종료 시간 기록 및 UI 갱신
+ _testEndTime = DateTime.Now;
+ txtTestEndTime.Text = "종료 시간: " + _testEndTime.ToString("yyyy-MM-dd HH:mm:ss");
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "결과 데이터베이스 저장 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
+ }
+
+ try
+ {
+ // 데이터베이스 저장 (비동기 스레드 격리로 UI 프리징 방지)
+ bool dbResult = await System.Threading.Tasks.Task.Run(() =>
{
- string serial = txtSerial.Text.Trim();
- _database.SaveProductResult(
+ return _database.SaveProductResult(
serial,
_loginInfo.Maker,
_loginInfo.Model,
@@ -305,26 +1216,108 @@ namespace marking_gui
_loginInfo.LineNo,
_loginInfo.LotNo,
_loginInfo.JigNo,
- finalVoltage,
- finalCurrent,
- finalResistance,
- isFinalPass ? "PASS" : "FAIL");
+ voltage,
+ current,
+ resistance,
+ result,
+ _testStartTime,
+ _testEndTime,
+ markedSerial,
+ _currentProduct?.PcbBarcode, // Housing_Assembly 테이블에서 조회한 PCB 바코드
+ _markingConfig.DrawingNo,
+ _markingConfig.HkmcNo);
+ });
- // 합격 판정 시 최종 SUCCESS 신호 전달 시뮬레이션
- if (isFinalPass)
+ if (dbResult)
+ {
+ LoggerService.Info($"[DB 저장 완료] S/N: {serial}, 결과: {result}, V: {voltage:F2}, I: {current:F2}, R: {resistance:F2}");
+
+
+
+
+ if (txtProgressStatus != null)
+ {
+ if (isFinalPass && (result == "PASS" || result == "OK"))
+ {
+ txtProgressStatus.Text = _database.OfflineMode ? "[오프라인] 각인 및 DB 모사 성공" : "모든 공정 완료 (DB 저장 성공)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green text
+ }
+ else if (isFinalPass)
+ {
+ txtProgressStatus.Text = "최종 합격 (마킹 실패)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange text
+ }
+ else
+ {
+ txtProgressStatus.Text = "최종 불합격 (DB 저장 성공)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
+ }
+ }
+
+ if (isFinalPass && (result == "PASS" || result == "OK"))
{
- System.Diagnostics.Debug.WriteLine(string.Format("[설비 통신] S/N: {0} | 최종 SUCCESS 신호를 외부 메모리에 전달했습니다.", serial));
+ LoggerService.Info($"[설비 통신] S/N: {serial} | 최종 SUCCESS 신호를 외부 메모리에 전달했습니다.");
}
+
+ // 실시간 각인 순번 화면 정보 갱신
+ RefreshNextSequence();
}
- catch (Exception ex)
+ else
{
- System.Diagnostics.Debug.WriteLine("DB 결과 저장 중 예외 발생: " + ex.Message);
+ LoggerService.Error($"[DB 저장 실패] S/N: {serial} 결과 저장 함수 실패 반환");
+
+ // 시스템 상태 텍스트에 DB 저장 실패 에러 내용을 그대로 표출
+ txtSystemStatus.Text = "DB 오류: 결과 데이터베이스 저장 실패 (DB 서버 연결/응답 없음)";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "공정 완료 (DB 저장 실패)";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
+ }
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = "DB 오류: 결과 데이터베이스 저장에 실패했습니다. DB 서버 접속 상태를 확인해 주십시오.";
+ }
+ if (borderErrorBanner != null)
+ {
+ borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ }
+ if (borderNormalStatus != null)
+ {
+ borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+ }
}
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error($"[DB 저장 예외] S/N: {serial} 결과 저장 오류 발생", ex);
- // 컨트롤 원상 복구 (재검사를 위해)
- txtSerial.IsEnabled = true;
- btnStart.IsEnabled = true;
+ txtSystemStatus.Text = "DB 예외: " + ex.Message;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "DB 저장 오류: " + ex.Message;
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
+ }
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = "DB 저장 예외: " + ex.Message;
+ }
+ if (borderErrorBanner != null)
+ {
+ borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ }
+ if (borderNormalStatus != null)
+ {
+ borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+ }
}
+
+ // 컨트롤 원상 복구 (재검사를 위해)
+ txtSerial.IsEnabled = true;
+ SetStartButtonReady();
}
// 화면 렌더링 완료 시 최초 로그인 팝업 구동
@@ -334,7 +1327,7 @@ namespace marking_gui
}
// 공통 로그인 팝업 처리
- private void ShowLoginPopup()
+ private async void ShowLoginPopup()
{
var loginWindow = new Housing.Login.StartupLoginWindow();
loginWindow.Owner = this; // 메인 윈도우 중앙 위에 표시되도록 Owner 지정
@@ -344,6 +1337,21 @@ namespace marking_gui
_loginInfo = loginWindow.Result;
DisplayLoginInfo();
ResetUI();
+ InitializeMarkingDevice();
+
+ // 로그인 완료 후 보드 상시전원(Channel 2) 인가를 완전 비동기 백그라운드 스레드로 실행 (UI 프리징 100% 방지)
+ _ = System.Threading.Tasks.Task.Run(async () =>
+ {
+ try
+ {
+ await _equipment.RunPreBoardCommandAsync();
+ LoggerService.Info("상시전원(Channel 2) 비동기 백그라운드 인가 완료");
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Warn($"상시전원 비동기 인가 스킵/예외: {ex.Message}");
+ }
+ });
}
else
{
@@ -361,6 +1369,19 @@ namespace marking_gui
return;
}
+ // 로그아웃 시 전원 차단 (LogoutCommand) 백그라운드 비동기 실행 (UI 블로킹 방지)
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await _equipment.RunLogoutCommandAsync();
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error("로그아웃 전원 OFF 실행 중 예외 발생", ex);
+ }
+ });
+
// 로그인 정보 클리어
_loginInfo = new Housing.Login.StartupLoginWindowResult { Operator = "---", Model = "---", LineNo = "---" };
DisplayLoginInfo();
@@ -370,6 +1391,98 @@ namespace marking_gui
ShowLoginPopup();
}
+ // 프로그램 강제 종료 시 전원 차단 및 LMC 마킹 장치 해제 연동 핸들러
+ private async void MainWindow_Closed(object sender, EventArgs e)
+ {
+ try
+ {
+ // DB 재연결 감시 루프 중단
+ _dbWatcherCts?.Cancel();
+
+ // LMC 마킹 기계 무선/시리얼 연결 핸들 안전 해제 (재시작 시 LMC 장치 락 방지)
+ _markingService?.Shutdown();
+
+ await _equipment.RunLogoutCommandAsync();
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error("프로그램 종료 시 전원 OFF 및 마킹 디바이스 해제 중 예외 발생", ex);
+ }
+ }
+
+ ///
+ /// DB 재연결 자동 감시 루프 - 독립 Task로 실행되며 UI 스레드를 차단하지 않음
+ /// 5초 간격으로 연결 상태를 체크하고, 재연결 감지 시 순번을 자동 갱신
+ ///
+ private void StartDbReconnectWatcher(CancellationToken token)
+ {
+ Task.Run(async () =>
+ {
+ LoggerService.Info("[DB 재연결 감시] 모니터링 시작 (5초 간격)");
+
+ while (!token.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(TimeSpan.FromSeconds(5), token);
+ }
+ catch (TaskCanceledException)
+ {
+ break;
+ }
+
+ if (token.IsCancellationRequested) break;
+
+ try
+ {
+ bool isNowConnected = _database.CheckConnection();
+
+ if (!isNowConnected && !_dbWasOffline)
+ {
+ // 연결됐다가 끊어진 경우
+ _dbWasOffline = true;
+ LoggerService.Warn("[DB 재연결 감시] DB 연결 끊김 감지");
+
+ Dispatcher.Invoke(() =>
+ {
+ if (txtSystemStatus != null)
+ {
+ txtSystemStatus.Text = "DB 연결 끊김 - 재연결 대기 중...";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
+ }
+ if (txtNextSequence != null)
+ txtNextSequence.Text = "연결 끊김";
+ });
+ }
+ else if (isNowConnected && _dbWasOffline)
+ {
+ // 재연결된 경우
+ _dbWasOffline = false;
+ LoggerService.Info("[DB 재연결 감시] DB 재연결 감지 - 순번 자동 갱신");
+
+ Dispatcher.Invoke(() =>
+ {
+ if (txtSystemStatus != null)
+ {
+ txtSystemStatus.Text = "DB 재연결 완료 - 각인 순번 갱신 중...";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
+ }
+ });
+
+ // UI 스레드에서 순번 갱신 (비동기 안전)
+ await Dispatcher.InvokeAsync(() => RefreshNextSequence());
+ }
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Warn($"[DB 재연결 감시] 체크 중 예외: {ex.Message}");
+ }
+ }
+
+ LoggerService.Info("[DB 재연결 감시] 모니터링 종료");
+ }, token);
+ }
+
private void BtnMinimize_Click(object sender, RoutedEventArgs e)
{
this.WindowState = WindowState.Minimized;
@@ -414,5 +1527,429 @@ namespace marking_gui
}
}
}
+
+ // 도안 파일 선택 버튼 클릭
+ private void btnSelectEzd_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog();
+
+ // 기본 파일 탐색 경로 설정: C:\EzCad\EasyCfg 폴더를 기본 탐색 경로로 지정
+ string initialDir = @"c:\EzCad\EasyCfg";
+ if (!System.IO.Directory.Exists(initialDir))
+ {
+ initialDir = _markingConfig.EzdFolderPath;
+ }
+ if (!System.IO.Directory.Exists(initialDir))
+ {
+ initialDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "EasyCfg");
+ }
+ if (System.IO.Directory.Exists(initialDir))
+ {
+ openFileDialog.InitialDirectory = initialDir;
+ }
+
+ openFileDialog.Filter = "EZD 도안 파일 (*.ezd)|*.ezd|모든 파일 (*.*)|*.*";
+ openFileDialog.Title = "EZD 도안 파일 선택";
+
+ if (openFileDialog.ShowDialog() == true)
+ {
+ string fullPath = openFileDialog.FileName;
+ SetSelectedEzdFile(fullPath, saveToConfig: true);
+ }
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("도안 파일을 여는 동안 오류가 발생했습니다:\n" + ex.Message, "오류", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void InitializeEzdFileSelection()
+ {
+ try
+ {
+ string targetFile = string.Empty;
+
+ // 1. Setting.ini 에서 이전에 선택했던 마지막 EZD 도안 파일 경로 로드
+ string savedFile = _markingConfig.LastSelectedEzdFile;
+ if (!string.IsNullOrWhiteSpace(savedFile) && System.IO.File.Exists(savedFile))
+ {
+ targetFile = savedFile;
+ }
+ else
+ {
+ // 2. 이전에 선택한 파일이 없거나 경로가 유효하지 않으면 C:\EzCad\EasyCfg 폴더에서 디폴트 탐색
+ string ezcadFolder = @"c:\EzCad\EasyCfg";
+ if (!System.IO.Directory.Exists(ezcadFolder))
+ {
+ ezcadFolder = _markingConfig.EzdFolderPath;
+ }
+ if (System.IO.Directory.Exists(ezcadFolder))
+ {
+ // AIC1.ezd 가 우선 존재하면 지정
+ string defaultAic = System.IO.Path.Combine(ezcadFolder, "AIC1.ezd");
+ if (System.IO.File.Exists(defaultAic))
+ {
+ targetFile = defaultAic;
+ }
+ else
+ {
+ // 그렇지 않으면 폴더 내 첫 번째 .ezd 파일 자동 선택
+ var ezdFiles = System.IO.Directory.GetFiles(ezcadFolder, "*.ezd");
+ if (ezdFiles.Length > 0)
+ {
+ targetFile = ezdFiles[0];
+ }
+ }
+ }
+ }
+
+ if (!string.IsNullOrWhiteSpace(targetFile) && System.IO.File.Exists(targetFile))
+ {
+ SetSelectedEzdFile(targetFile, saveToConfig: false);
+ }
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Warn("초기 EZD 도안 파일 로드 중 예외 발생: " + ex.Message);
+ }
+ }
+
+ private void SetSelectedEzdFile(string fullPath, bool saveToConfig = true)
+ {
+ if (string.IsNullOrWhiteSpace(fullPath) || !System.IO.File.Exists(fullPath)) return;
+
+ string fileName = System.IO.Path.GetFileName(fullPath);
+ _currentEzdFile = fullPath;
+ if (txtEzdFile != null) txtEzdFile.Text = fileName;
+
+ string modelName = System.IO.Path.GetFileNameWithoutExtension(fileName);
+ if (_loginInfo != null) _loginInfo.Model = modelName;
+ if (txtModelInfo != null) txtModelInfo.Text = modelName;
+
+ if (saveToConfig)
+ {
+ _markingConfig.SaveLastSelectedEzdFile(fullPath);
+ }
+ LoggerService.Info($"EZD 도안 파일 선택 완료: {fileName} (절대경로: {fullPath})");
+ }
+
+ // 테스트 마킹 버튼 클릭 (나머지 다 통과 가정한 레이저 각인 테스트)
+ private async void btnTestMarking_Click(object sender, RoutedEventArgs e)
+ {
+ try
+ {
+ string serial = txtSerial.Text.Trim();
+
+ // S/N 가 비어있는 경우, 테스트 마킹용 임시 S/N 발급
+ if (string.IsNullOrEmpty(serial))
+ {
+ serial = "T" + DateTime.Now.ToString("HHmmss");
+ txtSerial.Text = serial;
+ LoggerService.Info($"[테스트 각인] S/N가 비어있어 가상 S/N 발급: {serial}");
+ }
+
+ LoggerService.Info($"----------------- [테스트 각인 진행 시작] S/N: {serial} -----------------");
+
+ // 새로운 검사 진행을 위해 UI 상태를 초기화
+ ResetUI();
+
+ // UI 상태 복원 및 진행 개시 알림
+ txtSerial.Text = serial;
+ txtFinalSerial.Text = serial;
+
+ txtSystemStatus.Text = "상태: 테스트 마킹 시작 (이전 단계 패스 가정)...";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
+
+ txtFinalResult.Text = "테스트 중";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue text
+ if (borderVerdictCard != null)
+ {
+ borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(239, 246, 255)); // Light Blue background
+ borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue border
+ borderVerdictCard.BorderThickness = new Thickness(2);
+ }
+ if (badgeFinalResult != null)
+ {
+ badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white
+ badgeFinalResult.BorderBrush = new SolidColorBrush(Color.FromRgb(229, 231, 235)); // Gray
+ }
+ borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "[테스트] 모의 계측 진행 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue text
+ }
+
+ SetStartButtonBusy();
+ btnTestMarking.IsEnabled = false;
+ txtSerial.IsEnabled = false;
+
+ // 이전 단계 결과를 강제로 다 OK로 설정
+ txtPrevHousing.Text = "OK";
+ txtPrevHousing.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
+ txtPrevCal.Text = "OK";
+ txtPrevCal.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
+ txtPrevEol.Text = "OK";
+ txtPrevEol.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
+
+ // 시작 시간 기록 및 UI 출력
+ _testStartTime = DateTime.Now;
+ txtTestStartTime.Text = "시작 시간: " + _testStartTime.ToString("yyyy-MM-dd HH:mm:ss");
+ txtTestEndTime.Text = "종료 시간: --:--:--";
+
+ // 측정값을 정상 범위의 중간값으로 가정하여 설정 (전압 5.0V, 전류 9.0A, 저항 0.6Ω)
+ double mockVoltage = (_limits.MinVoltage + _limits.MaxVoltage) / 2.0;
+ double mockCurrent = (_limits.MinCurrent + _limits.MaxCurrent) / 2.0;
+ double mockResistance = (_limits.MinResistance + _limits.MaxResistance) / 2.0;
+
+ txtVoltage.Text = mockVoltage.ToString("F2");
+ txtCurrent.Text = mockCurrent.ToString("F2");
+ txtResistance.Text = mockResistance.ToString("00.000");
+
+ txtVoltage.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
+ txtCurrent.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
+ txtResistance.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
+
+ if (borderVoltage != null)
+ {
+ borderVoltage.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E"));
+ borderVoltage.BorderThickness = new Thickness(3);
+ }
+ if (borderCurrent != null)
+ {
+ borderCurrent.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E"));
+ borderCurrent.BorderThickness = new Thickness(3);
+ }
+ if (borderResistance != null)
+ {
+ borderResistance.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E"));
+ borderResistance.BorderThickness = new Thickness(3);
+ }
+
+ // 최종 판정 OK 설정
+ txtSystemStatus.Text = "상태: 검사 완료 (합격 가정)";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
+
+ txtFinalResult.Text = "OK";
+ txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green text for white card
+ if (borderVerdictCard != null)
+ {
+ borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(240, 253, 250)); // Light Green background
+ borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green border
+ borderVerdictCard.BorderThickness = new Thickness(3);
+ }
+ if (badgeFinalResult != null)
+ {
+ badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white
+ }
+ borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
+
+ // 마킹 상태 설정
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 중...";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange
+
+ if (txtProgressStatus != null)
+ {
+ txtProgressStatus.Text = "[테스트] 레이저 각인 진행 중...";
+ txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange text
+ }
+
+ // 비동기 마킹 동작 수행
+ await System.Threading.Tasks.Task.Run(async () =>
+ {
+ try
+ {
+ // 1. 테스트용 각인 일련번호 생성 (yyMMdd + 4자리 시퀀스)
+ string currentLot = string.IsNullOrEmpty(_loginInfo.LotNo) || _loginInfo.LotNo == "---" ? "TESTLOT" : (_loginInfo.LotNo.Trim().Length < 2 ? _loginInfo.LotNo.Trim().PadLeft(2, '0') : _loginInfo.LotNo.Trim());
+ int seq;
+ try
+ {
+ seq = await GetNextMarkingSequenceAsync(currentLot);
+ }
+ catch (Exception seqEx)
+ {
+ LoggerService.Error("[테스트 순번 조회 실패] DB 순번 조회 실패", seqEx);
+ Dispatcher.Invoke(() =>
+ {
+ string errMsg = $"각인 순번 DB 조회 실패: {seqEx.Message}";
+ if (txtSystemStatus != null)
+ {
+ txtSystemStatus.Text = errMsg;
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ if (txtErrorMessage != null) txtErrorMessage.Text = errMsg;
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+ MessageBox.Show($"각인 순번을 DB에서 가져오지 못했습니다.\n\n{seqEx.Message}\n\nDB 연결 상태를 확인하십시오.", "순번 조회 실패", MessageBoxButton.OK, MessageBoxImage.Error);
+ });
+ return;
+ }
+
+ string yymmdd = DateTime.Now.ToString("yyMMdd");
+ string markedSerial = string.Format("{0}{1}{2:D4}", yymmdd, currentLot, seq);
+ string testSerial = string.IsNullOrEmpty(serial) ? "TEST_IC_SN" : serial;
+ string drawingNo = _markingConfig.DrawingNo;
+ string hkmcNo = _markingConfig.HkmcNo;
+ string qrString = string.Format("{0};{1};{2};{3};", testSerial, drawingNo, hkmcNo, markedSerial);
+
+ // 마킹 스캔 요청 데이터 빌드
+ var req = new MarkingRequest
+ {
+ Serial = markedSerial,
+ IcSerial = testSerial,
+ LotNo = currentLot,
+ ProductNo = string.IsNullOrEmpty(_loginInfo.Model) || _loginInfo.Model == "---" ? "AIC" : _loginInfo.Model,
+ DrawingNo = drawingNo,
+ HkmcNo = hkmcNo,
+ EzdFileName = _currentEzdFile,
+ LineCode = string.IsNullOrEmpty(_loginInfo.LineNo) || _loginInfo.LineNo == "---" ? "L1" : _loginInfo.LineNo,
+ ShiftCode = "A", // 기본 1조
+ QrString = qrString
+ };
+
+ MarkingResult markingRes;
+ if (_database.OfflineMode)
+ {
+ // 오프라인 모드 시뮬레이션
+ await System.Threading.Tasks.Task.Delay(500);
+ markingRes = new MarkingResult
+ {
+ Success = true,
+ ErrorCode = 0,
+ ErrorMessage = "오프라인 모드 각인 완료 모사",
+ MarkedAt = DateTime.Now
+ };
+ }
+ else
+ {
+ markingRes = _markingService.RequestMarking(req);
+ }
+
+ // UI 스레드로 컴백
+ Dispatcher.Invoke(() =>
+ {
+ if (txtMarkedSerial != null)
+ {
+ txtMarkedSerial.Text = markedSerial;
+ }
+
+ if (markingRes.Success)
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 성공";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
+ LoggerService.Info($"[테스트 각인 성공] S/N: {serial}, 각인번호: {markedSerial}");
+ }
+ else
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 실패 (" + markingRes.ErrorCode + ")";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
+ LoggerService.Warn($"[테스트 각인 실패] S/N: {serial}, 각인번호: {markedSerial}, Code: {markingRes.ErrorCode}, Message: {markingRes.ErrorMessage}");
+ MessageBox.Show(
+ string.Format("테스트 레이저 각인 작업 전송 실패\n\n에러 코드: {0}\n에러 메시지: {1}\n\n장치 초기화 및 설정 상태를 점검하십시오.",
+ markingRes.ErrorCode, markingRes.ErrorMessage),
+ "테스트 각인 실패", MessageBoxButton.OK, MessageBoxImage.Warning);
+
+ if (txtErrorMessage != null)
+ {
+ txtErrorMessage.Text = string.Format("테스트 각인 에러: {0} ({1})", markingRes.ErrorMessage, markingRes.ErrorCode);
+ }
+ if (borderErrorBanner != null)
+ {
+ borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
+ }
+ if (borderNormalStatus != null)
+ {
+ borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
+ }
+ }
+
+ // DB 저장 및 마무리 (마킹 성공 시 최종 OK로 저장)
+ SaveAndFinalize(serial, mockVoltage, mockCurrent, mockResistance, true, markingRes.Success ? "OK" : "NG", markedSerial);
+
+ // 테스트 마킹 버튼 활성화 복구
+ btnTestMarking.IsEnabled = true;
+ });
+ }
+ catch (Exception threadEx)
+ {
+ Dispatcher.Invoke(() =>
+ {
+ if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 에러";
+ if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ LoggerService.Error($"[테스트 각인 비동기 스레드 에러] S/N: {serial}", threadEx);
+ MessageBox.Show("각인 하드웨어 제어 중 치명적인 예외가 발생했습니다:\n" + threadEx.Message + "\n" + threadEx.StackTrace, "마킹 엔진 오류", MessageBoxButton.OK, MessageBoxImage.Error);
+
+ btnTestMarking.IsEnabled = true;
+ SetStartButtonReady();
+ txtSerial.IsEnabled = true;
+ });
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("테스트 마킹 프로세스 진행 중 예외 발생:\n" + ex.Message + "\n" + ex.StackTrace, "오류", MessageBoxButton.OK, MessageBoxImage.Error);
+ btnTestMarking.IsEnabled = true;
+ SetStartButtonReady();
+ txtSerial.IsEnabled = true;
+ }
+ }
+ private async Task GetNextMarkingSequenceAsync(string lotNo)
+ {
+ if (string.IsNullOrWhiteSpace(lotNo)) return 1;
+
+ return await System.Threading.Tasks.Task.Run(() =>
+ {
+ int seq = _database.GetNextMarkingSequence(lotNo);
+ return seq > 0 ? seq : 1;
+ });
+ }
+
+ private async void RefreshNextSequence()
+ {
+ if (_loginInfo == null || string.IsNullOrEmpty(_loginInfo.LotNo))
+ {
+ if (txtNextSequence != null) txtNextSequence.Text = "--- 차례";
+ return;
+ }
+
+ try
+ {
+ int nextSeq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo);
+ if (txtNextSequence != null)
+ {
+ txtNextSequence.Text = string.Format("{0:D4} 차례", nextSeq);
+ }
+
+ if (nextSeq > 9999)
+ {
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Collapsed;
+ if (txtErrorMessage != null) txtErrorMessage.Text = "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오.";
+ }
+ else
+ {
+ if (txtErrorMessage != null && txtErrorMessage.Text.Contains("각인 순번이 9999를 초과했습니다"))
+ {
+ if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Collapsed;
+ if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Visible;
+ txtErrorMessage.Text = string.Empty;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ LoggerService.Error("[UI RefreshSequence] 순번 갱신 중 에러", ex);
+ if (txtNextSequence != null) txtNextSequence.Text = "조회 실패";
+ if (txtSystemStatus != null)
+ {
+ txtSystemStatus.Text = $"각인 순번 조회 실패: {ex.Message}";
+ txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
+ }
+ }
+ }
}
}
diff --git a/marking_gui/Manual/.obsidian/app.json b/marking_gui/Manual/.obsidian/app.json
new file mode 100644
index 0000000..1866230
--- /dev/null
+++ b/marking_gui/Manual/.obsidian/app.json
@@ -0,0 +1,3 @@
+{
+ "spellcheck": false
+}
\ No newline at end of file
diff --git a/marking_gui/Manual/.obsidian/appearance.json b/marking_gui/Manual/.obsidian/appearance.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/marking_gui/Manual/.obsidian/appearance.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/marking_gui/Manual/.obsidian/core-plugins.json b/marking_gui/Manual/.obsidian/core-plugins.json
new file mode 100644
index 0000000..639b90d
--- /dev/null
+++ b/marking_gui/Manual/.obsidian/core-plugins.json
@@ -0,0 +1,33 @@
+{
+ "file-explorer": true,
+ "global-search": true,
+ "switcher": true,
+ "graph": true,
+ "backlink": true,
+ "canvas": true,
+ "outgoing-link": true,
+ "tag-pane": true,
+ "footnotes": false,
+ "properties": true,
+ "page-preview": true,
+ "daily-notes": true,
+ "templates": true,
+ "note-composer": true,
+ "command-palette": true,
+ "slash-command": false,
+ "editor-status": true,
+ "bookmarks": true,
+ "markdown-importer": false,
+ "zk-prefixer": false,
+ "random-note": false,
+ "outline": true,
+ "word-count": true,
+ "slides": false,
+ "audio-recorder": false,
+ "workspaces": false,
+ "file-recovery": true,
+ "publish": false,
+ "sync": true,
+ "bases": true,
+ "webviewer": false
+}
\ No newline at end of file
diff --git a/marking_gui/Manual/.obsidian/workspace.json b/marking_gui/Manual/.obsidian/workspace.json
new file mode 100644
index 0000000..6ae25c3
--- /dev/null
+++ b/marking_gui/Manual/.obsidian/workspace.json
@@ -0,0 +1,196 @@
+{
+ "main": {
+ "id": "e986c9dffdb55d9e",
+ "type": "split",
+ "children": [
+ {
+ "id": "e5576011528e6b5c",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "b98d55f22f4d8058",
+ "type": "leaf",
+ "state": {
+ "type": "markdown",
+ "state": {
+ "file": "02_초기_설정.md",
+ "mode": "source",
+ "source": false
+ },
+ "icon": "lucide-file",
+ "title": "02_초기_설정"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "vertical"
+ },
+ "left": {
+ "id": "c57355fcc7fdff6f",
+ "type": "split",
+ "children": [
+ {
+ "id": "0dd5370634e2a0cb",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "34581207ae14950d",
+ "type": "leaf",
+ "state": {
+ "type": "file-explorer",
+ "state": {
+ "sortOrder": "alphabetical",
+ "autoReveal": false
+ },
+ "icon": "lucide-folder-closed",
+ "title": "파일 탐색기"
+ }
+ },
+ {
+ "id": "9392a937e5fa675d",
+ "type": "leaf",
+ "state": {
+ "type": "search",
+ "state": {
+ "query": "",
+ "matchingCase": false,
+ "explainSearch": false,
+ "collapseAll": false,
+ "extraContext": false,
+ "sortOrder": "alphabetical"
+ },
+ "icon": "lucide-search",
+ "title": "검색"
+ }
+ },
+ {
+ "id": "885558dab49fcbe9",
+ "type": "leaf",
+ "state": {
+ "type": "bookmarks",
+ "state": {},
+ "icon": "lucide-bookmark",
+ "title": "북마크"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "width": 300
+ },
+ "right": {
+ "id": "99e0f4ec73247bac",
+ "type": "split",
+ "children": [
+ {
+ "id": "e73c85dfbcab7caf",
+ "type": "tabs",
+ "children": [
+ {
+ "id": "563edc0360ae71a5",
+ "type": "leaf",
+ "state": {
+ "type": "backlink",
+ "state": {
+ "file": "06_데이터_조회.md",
+ "collapseAll": false,
+ "extraContext": false,
+ "sortOrder": "alphabetical",
+ "showSearch": false,
+ "searchQuery": "",
+ "backlinkCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-coming-in",
+ "title": "06_데이터_조회 의 백링크"
+ }
+ },
+ {
+ "id": "5a190c7cdaac43a6",
+ "type": "leaf",
+ "state": {
+ "type": "outgoing-link",
+ "state": {
+ "file": "06_데이터_조회.md",
+ "linksCollapsed": false,
+ "unlinkedCollapsed": true
+ },
+ "icon": "links-going-out",
+ "title": "06_데이터_조회 의 나가는 링크"
+ }
+ },
+ {
+ "id": "b2a637895408e226",
+ "type": "leaf",
+ "state": {
+ "type": "tag",
+ "state": {
+ "sortOrder": "frequency",
+ "useHierarchy": true,
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-tags",
+ "title": "태그"
+ }
+ },
+ {
+ "id": "c2aa104400708a18",
+ "type": "leaf",
+ "state": {
+ "type": "all-properties",
+ "state": {
+ "sortOrder": "frequency",
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-archive",
+ "title": "모든 속성"
+ }
+ },
+ {
+ "id": "210708f9a623a5be",
+ "type": "leaf",
+ "state": {
+ "type": "outline",
+ "state": {
+ "file": "06_데이터_조회.md",
+ "followCursor": false,
+ "showSearch": false,
+ "searchQuery": ""
+ },
+ "icon": "lucide-list",
+ "title": "06_데이터_조회 의 개요"
+ }
+ }
+ ]
+ }
+ ],
+ "direction": "horizontal",
+ "width": 300,
+ "collapsed": true
+ },
+ "left-ribbon": {
+ "hiddenItems": {
+ "switcher:빠른 전환기 열기": false,
+ "graph:그래프 뷰 열기": false,
+ "canvas:새 캔버스 만들기": false,
+ "daily-notes:오늘의 일일 노트 열기": false,
+ "templates:템플릿 삽입": false,
+ "command-palette:명령어 팔레트 열기": false,
+ "bases:새 베이스 생성하기": false
+ }
+ },
+ "active": "b98d55f22f4d8058",
+ "lastOpenFiles": [
+ "Manual_for_pdf.html",
+ "05_시험_설정.md",
+ "04_통신_설정.md",
+ "03_메인_화면.md",
+ "02_초기_설정.md",
+ "01_시스템_개요.md",
+ "06_데이터_조회.md"
+ ]
+}
\ No newline at end of file
diff --git a/marking_gui/Manual/01_시스템_개요.md b/marking_gui/Manual/01_시스템_개요.md
new file mode 100644
index 0000000..f4647fb
--- /dev/null
+++ b/marking_gui/Manual/01_시스템_개요.md
@@ -0,0 +1,67 @@
+# 1. 시스템 개요 및 요구사항
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 1.1 시스템 개요
+
+**Performance Inspect & Laser Marking System**은 온압센서 완제품의 최종 성능 검사를 수행하고, 이전 공정 단계들의 합격(PASS) 이력을 조회하여 최종 적합 제품에 한해 제품 표면에 레이저 마킹을 수행하는 시스템 제어 프로그램입니다.
+
+본 프로그램은 측정 계측기와 센서 판독기, 레이저 각인 장치 및 공장 생산 데이터베이스와 연동되어 작동합니다.
+
+### 주요 기능 요약
+
+| 기능 | 설명 |
+|------|------|
+| **일련번호(S/N) 자동 추출** | 센서 판독 보드를 통해 제품 안착 시 제품 내부 메모리로부터 일련번호를 추출합니다. |
+| **선행 공정 검사 교차 조회** | 이전 단계(하우징 조립, 센서 보정, 1차 EOL 성능 검사)가 모두 합격 판정되었는지 생산 데이터베이스에서 조회합니다. |
+| **자동 계측 시퀀스** | 전원 공급 장치와 전압/저항 측정 장비들과 연동하여 제품에 테스트 전원을 투입하고 전압, 저항, 전류 값을 차례대로 계측합니다. |
+| **실제 저항값 측정** | 전압/저항 DMM 측정기를 통해 센서의 실제 저항값(Ω)을 측정합니다. |
+| **전류 측정** | 전원 공급 장치(Power Supply)의 출력 채널 1을 통해 제품의 소모 전류를 계측합니다. |
+| **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 2D QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. |
+| **결과 기록** | 계측 판정 규격(전압, 전류, 저항)과 마킹 성공 여부를 최종 판정하여 생산 데이터베이스에 저장합니다. |
+
+---
+
+## 1.2 소프트웨어 요구사항
+
+| 항목 | 요구사항 | 비고 |
+|------|----------|------|
+| **통합 설치 프로그램** | `MarkingSetup.exe` (WiX 번들) | 1-Click 자동 설치 및 실행 환경, 드라이버, 설정 일체 셋업 |
+| **운영체제** | Windows 10 이상 | 프로그램 구동 및 장비 드라이버 운용용 |
+| **데이터베이스** | Microsoft SQL Server (MS-SQL) | 생산 공정 데이터 및 계측 결과 연동용 |
+| **레이저 제어 프로그램** | EZCAD 각인 제어 프로그램 및 드라이버 | 레이저 마킹기 구동 및 전용 도면(.ezd) 운용용 |
+| **네트워크 환경** | LAN (TCP/IP) 통신 | 제어 PC와 측정 기기 간의 연결 |
+| **센서 보드 통신** | COM 포트 (RS-232 시리얼) | 4251 센서 통신 보드 연동 |
+
+---
+
+## 1.3 하드웨어 구성
+
+| 장비 | 역할 | 통신 방식 |
+|------|------|-----------|
+| **센서 판독 보드 (4251 보드)** | 제품 내부 칩의 일련번호(IC_SN)를 인식합니다. | RS-232 시리얼 통신 |
+| **전압 및 저항 측정 장비 (DMM)** | 제품의 전압(V) 및 전기적 저항(Ω)을 측정합니다. (예: 192.168.1.100) | TCP/IP 네트워크 통신 |
+| **전류 측정 및 전원 공급 장비 (PSU)** | 제품 구동을 위한 전원 공급 및 소모 전류(A)를 측정합니다. (예: 192.168.1.101) | TCP/IP 네트워크 통신 |
+| **레이저 마킹기** | 2D QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 |
+| *(전류 측정 DMM)* | *설정 파일(예: 192.168.1.102)에 존재하나 현재 제어 시퀀스에서는 사용하지 않습니다.* | *(미사용)* |
+
+## 1.4 설치 경로(`C:\marking\`) 구조 및 생성 파일 역할
+
+단일 통합 설치 마법사(`MarkingSetup.exe`)를 통해 시스템을 설치하면 기본 설치 디렉터리(`C:\marking\`) 아래에 다음과 같은 실행 바이너리, 설정 파일, 드라이버 라이브러리 및 런타임 로그 폴더가 배치·생성됩니다.
+
+| 구분 | 파일 / 폴더명 | 상세 역할 및 기능 설명 |
+|------|----------------|------------------------|
+| **실행 프로그램** | `marking_gui.exe` | 메인 성능 검사 및 레이저 마킹 GUI 제어 실행 파일 |
+| **종속 라이브러리** | `marking_gui.dll`, `*.deps.json`, `*.runtimeconfig.json` | .NET 9.0 (WPF) 자립형 런타임 및 종속 구성 바이너리 |
+| **통합 설정 파일** | `Setting.ini` | 마킹기 폴더 경로, 테스트 모드, 도안 태그 매핑, 계측기 LAN IP, SCPI 전원 커맨드, 계측 판정 규격 임계치(`[Limits]`) 종합 설정 파일 |
+| **DB 설정 파일** | `Database.ini` | MS-SQL 데이터베이스 접속 주소, DB명, 사용자 계정/PW, 오프라인 모드(`OfflineMode=true`) 설정 파일 |
+| **센서 설정 파일** | `Sensor.ini` | 4251 센서 통신 보드 시리얼 포트(`Port=COM3`), 통신 속도(`BaudRate=115200`), 통신 채널(`Channel=1`) 설정 파일 |
+| **드라이버 폴더** | `libs/` | C++ LMC 각인 드라이버 격리 디렉터리 (`MarkEzdWrapper.dll`, `DataMgr.dll`, `MarkEzd.dll`, `haspms32.dll` 등) |
+| **로그 저장 폴더** | `logs/` *(자동 생성)* | 시스템 운용 중 발생하는 일별 이벤트 로그, 통신 이력, 각인 성공/실패 기록 텍스트 저장 디렉터리 (예: `logs/2026-07-23.log`) |
+| **백업 보관 폴더** | `publish_backup/` | 프로그램 업데이트 및 빌드 배포 시 이전 릴리즈 바이너리/설정 자동 백업 폴더 |
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/02_초기_설정.md b/marking_gui/Manual/02_초기_설정.md
new file mode 100644
index 0000000..6327b90
--- /dev/null
+++ b/marking_gui/Manual/02_초기_설정.md
@@ -0,0 +1,117 @@
+# 2. 초기 설정 가이드
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 2.1 프로그램 설정 구성 개요
+
+본 프로그램은 정상적인 계측기 통신 및 데이터베이스 연동을 위해 몇 가지 설정 파일을 사용합니다. 이 설정 파일들은 프로그램 설치 및 라인 도입 시 최초 1회 셋업을 원칙으로 하며, 일반 운영자가 직접 설정 파일을 열어 수정할 필요는 없습니다.
+
+*참고: 대부분의 검사 기준 및 통신 정보는 프로그램 내의 **'통신 설정'** 및 **'시험 설정'** 화면을 통해 수정할 수 있습니다.*
+
+### 2.1.1 주요 설정 정보 구분
+
+1. **장비 및 계측 설정**
+ - 계측기들과의 네트워크 연결 주소와 대기 시간 등을 정의합니다.
+ - 제품 합격 여부를 판정하는 전압, 전류, 저항의 기본 합격 규격 범위가 이 파일에 저장되며, 화면 내의 **'시험 설정'** 창에서 수정한 사항이 이 파일에 동적으로 반영됩니다.
+ - 프로그램은 전압/저항 측정 장치와 전원/전류 공급 장치를 제어하여 계측을 진행합니다.
+
+2. **생산 데이터베이스 연동 설정**
+ - 공장 생산 관리 시스템 데이터베이스와의 통신 경로가 기록되어 있습니다.
+ - 전산망이나 DB 서버 점검 등으로 인해 통신이 어려울 경우, 설정 파일을 통해 **'오프라인 모드'**로 전환하여 단독으로 장비 작동 테스트를 해볼 수 있습니다.
+ - **오프라인 로그인 지원**: 데이터베이스 확인 단계 없이 로그인하여 프로그램을 기동합니다.
+ - **오프라인 검사 지원**: 제품 이력 조회 및 검사 결과 저장을 건너뛰고 단독으로 계측 및 마킹을 실행합니다.
+
+3. **센서 판독기 설정**
+ - 제품 일련번호를 로드하기 위한 통신 채널 및 연결 포트 주소가 정의됩니다.
+ - 화면 내의 **'통신 설정'** 창에서 수정한 사항이 이 파일에 자동으로 저장됩니다.
+
+### 2.1.2 설치 경로(`C:\marking\`) 내 파일 배치 구조
+
+프로그램 설치 완료 후 `C:\marking\` 경로에는 다음과 같이 구동 파일, 설정 파일, 드라이버 라이브러리 및 런타임 자동 생성 디렉터리가 배치됩니다.
+
+* **`C:\marking\marking_gui.exe`**: GUI 제어 메인 실행 파일
+* **`C:\marking\Setting.ini`**: 장비 통신 IP, 마킹 태그, 규격 임계치(`[Limits]`) 종합 설정 파일
+* **`C:\marking\Database.ini`**: MS-SQL DB 접속 정보 및 오프라인 모드 설정 파일
+* **`C:\marking\Sensor.ini`**: 4251 센서 통신 보드 포트/속도/채널 설정 파일
+* **`C:\marking\libs\`**: C++ LMC 레이저 각인 드라이버 라이브러리 격리 폴더 (`MarkEzdWrapper.dll` 등)
+* **`C:\marking\logs\`**: 런타임 가동 중 자동 생성되는 일별 이력 로그 폴더 (`yyyy-MM-dd.log`)
+* **`C:\marking\publish_backup\`**: 업데이트/배포 시 이전 버전 바이너리 및 설정 자동 백업 폴더
+
+### 2.1.3 기본적인 설정 파일 편집 방법
+
+> [!IMPORTANT]
+> **[권장 조치 안내]**
+> 현장 라인 검사자 및 일반 작업자는 프로그램 UI의 **[통신 설정]** 및 **[시험 설정]** 메뉴를 통해 안전하게 파라미터를 변경하십시오.
+> 아래의 `.ini` 파일 직접 수정은 라인 엔지니어 또는 설비 관리자 전용 권한 작업이며, 잘못된 파일 수정 시 장비 통신 오류가 발생할 수 있습니다.
+
+설비 이동이나 네트워크 환경 변경 시, 프로그램 폴더 내의 각 `.ini` 파일을 텍스트 편집기(메모장 등)로 열어 아래의 전체 설정 레이아웃 항목을 참조하여 수정할 수 있습니다.
+
+#### 1) Setting.ini (주요 변경 가능 설정)
+현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다.
+```ini
+[Settings]
+EzdFolderPath=c:\EzCad\EasyCfg ; EZD 도안 파일 기본 탐색/저장 폴더 경로
+test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false)
+
+[DMM]
+VoltageHost=192.168.1.100 ; 전압/저항 측정용 계측기(DMM) IP 주소
+
+[CurrentDMM]
+CurrentDMMHost=192.168.1.102 ; 전류 측정용 계측기(DMM) IP 주소
+
+[PowerSupply]
+CurrentHost=192.168.1.101 ; 전원 공급기(Power Supply) IP 주소
+
+[Limits]
+MinVoltage=4.0 ; 전압 하한 판정 기준값 (V)
+MaxVoltage=6.0 ; 전압 상한 판정 기준값 (V)
+MinCurrent=8.0 ; 전류 하한 판정 기준값 (mA)
+MaxCurrent=10.0 ; 전류 상한 판정 기준값 (mA)
+MinResistance=0.4 ; 저항 하한 판정 기준값 (kΩ)
+MaxResistance=0.8 ; 저항 상한 판정 기준값 (kΩ)
+```
+
+#### 2) Database.ini (주요 변경 가능 설정)
+DB 서버 접속 주소 및 점검용 오프라인 모드 변경 시 수정하는 항목입니다.
+```ini
+[Database]
+Db_Server=192.168.1.200 ; MSSQL 데이터베이스 서버 IP 주소
+Db_Name=MarkingDb ; 연결할 데이터베이스 이름
+Db_User=db_user ; DB 접속 사용자 계정
+Db_Password=db_password ; DB 접속 비밀번호
+OfflineMode=False ; DB 미연결 점검 모드 (True: DB 저장 생략, False: 정상 검증/적재)
+
+[Login]
+OfflinePreview=False ; 로그인 DB 검증 우회 여부 (True: 로그인 생략, False: 정상 검증)
+```
+
+#### 3) Sensor.ini (주요 변경 가능 설정)
+바코드 스캐너 시리얼 포트 및 채널 설정 항목입니다.
+```ini
+[Sensor]
+Port=COM3 ; 바코드 스캐너 시리얼 포트 (장치 관리자 COM 포트 번호)
+Channel=1 ; 통신 채널 번호 (1 또는 2)
+```
+
+---
+
+## 2.2 최초 실행 및 장비 점검 순서
+
+라인 가동 전 최초 실행할 때는 아래 단계를 순서대로 이행하여 정상 작동 여부를 점검하십시오.
+
+1. **프로그램 단일 설치 (MarkingSetup.exe)**
+ - 배포받은 단일 설치 마법사 `MarkingSetup.exe`를 실행하여 1-Click으로 `C:\marking\` 설치 및 바탕화면 바로가기를 자동 생성합니다.
+2. **데이터베이스 연결 상태 확인**
+ - 데이터베이스가 점검 중인 경우 설정 파일을 통해 오프라인 모드로 설정하면 로그인 창을 통과하여 단독 테스트를 진행할 수 있습니다.
+3. **장비 네트워크 통신 점검**
+ - 제어 PC와 계측 장비들(전압/저항 측정 장비, 전원 공급 장치) 간의 LAN 케이블 연결을 확인합니다.
+ - 제어 PC의 네트워크 IP 환경이 장비들의 대역과 동일한지 확인하고, 통신 신호가 정상적으로 송수신되는지 점검합니다.
+4. **상시전원 자동 인가 확인**
+ - 프로그램 실행 후 작업자 계정으로 로그인을 시도합니다.
+ - 로그인이 완료되면 전원 공급 장치의 상시전원 채널 전원 램프가 자동으로 켜지는지 확인합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/03_메인_화면.md b/marking_gui/Manual/03_메인_화면.md
new file mode 100644
index 0000000..70ff798
--- /dev/null
+++ b/marking_gui/Manual/03_메인_화면.md
@@ -0,0 +1,63 @@
+# 3. 메인 화면 (Home View)
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 3.1 메인 화면 레이아웃 구성
+
+본 프로그램은 작업자가 화면 구성 및 각 항목 상태를 파악할 수 있도록 4개의 카드 레이아웃으로 화면을 구성했습니다.
+
+### 3.1.1 상단 헤더 영역 (Header Bar)
+
+프로그램 최상단 영역에는 시스템 정보 배지와 제어 버튼이 배치되어 있습니다.
+* **실시간 시계**: 좌측 상단에 날짜와 시간이 표시됩니다.
+* **작업 정보 배지**:
+ * **공정 라인**: 로그인 단계에서 지정한 생산 라인 코드 (`Line No`)
+ * **로트 번호**: 로그인 단계에서 지정한 생산 로트 번호 (`Lot No`)
+ * **모델명**: 현재 가동 중인 제품의 사양 모델 (`Model`)
+ * **작업자**: 로그인한 작업자의 이름 (`Operator`)
+* **제어 버튼**: 우측 상단에 `로그아웃` 버튼이 위치하며 최하단 바에 설정 관련 버튼이 배치됩니다.
+
+---
+
+## 3.2 상태 및 제어 카드 영역
+
+화면 중앙에는 역할을 구분한 4개의 상태 카드가 배치되어 있어, 제품 상태와 계측 단계를 모니터링할 수 있습니다.
+
+### 3.2.1 [카드 1] 제품 정보 및 이전 결과 (좌측 상단)
+제품의 일련번호와 이전 생산 공정에서의 합격 이력을 대조하는 영역입니다.
+* **작업 모델 설정**:
+ - `파일 선택` 버튼을 사용하여 각인용 도면 파일을 불러옵니다.
+ - 도면 파일 선택이 완료되면 상단 헤더 영역의 모델명(Model) 배지 텍스트가 선택한 도면명으로 자동 변경됩니다.
+* **일련번호 (IC_SN)**: 제품이 안착되면 센서 판독기를 통해 자동으로 인식되어 표시됩니다. (읽기 전용)
+* **이전 공정 이력 배지**:
+ * **Housing**: 하우징 조립 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
+ * **Calibration**: 압력 보정 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
+ * **EOL**: EOL 1차 성능 평가 판정 상태 (`PASS` / `FAIL` / `대기`)
+ * *주의: 이전 공정 이력 배지 중 하나라도 `FAIL` 상태인 경우 성능 검사를 시작할 수 없습니다.*
+
+### 3.2.2 [카드 2] 측정 결과 (우측 상단)
+현재 장비에서 실제 계측한 3대 전기 파라미터(전압, 전류, 저항) 수치와 설정된 기준 규격 범위를 표시하는 영역입니다.
+* **측정 전압 (Voltage)**: 전압 측정 장치가 측정한 전압 수치 (**V** 단위)
+* **측정 전류 (Current)**: 전원 공급 장치가 측정한 소모 전류 수치 (**mA** 단위)
+* **측정 저항 (Resistance)**: 저항 측정 장치가 측정한 전기적 저항 수치 (**kΩ** 단위)
+* *알림: 각 물리량의 하단에는 현재 적용 중인 기준 규격 범위가 문구(`기준범위: X ~ Y`)로 표출되며, 실측값이 규격 범위 내에 도달하면 카드 테두리가 **초록색(PASS)**으로 표시되고, 범위를 벗어나면 테두리가 **빨간색(FAIL)**으로 바뀝니다.*
+
+### 3.2.3 [카드 3] 진행상황 (좌측 하단)
+현재 설비가 수행하고 있는 단계별 자동 제어 시퀀스를 보여주는 안내 창입니다.
+* "대기 상태", "제품 정보 수집 중...", "이전 공정 검사 결과 조회 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 작동 상태를 실시간 텍스트로 표출합니다.
+
+### 3.2.4 [카드 4] 판정결과 (우측 하단)
+제품의 최종 합격(PASS) 및 불합격(FAIL) 상태를 표시합니다.
+* 계측 검사와 데이터 저장, 레이저 각인이 완료되었을 때 최종 합격인 경우 카드 배경이 **초록색(PASS)**으로 전환되며, 불합격인 경우 카드 배경이 **빨간색(FAIL)**으로 변경됩니다.
+
+### 3.2.5 최하단 제어 영역 (Control Box)
+* **검사 시작 (START)**: 전체 검사 시퀀스를 기동하는 메인 버튼입니다. 이 버튼을 누르면 이전 계측 수치들이 초기화되고 새로운 측정 프로세스가 개시됩니다.
+* **강제 검사 (스킵)**: 이전 가공 공정의 합격 이력이 없더라도 검증 단계를 건너뛰고 성능 계측 및 각인 시퀀스를 기동하는 버튼입니다.
+* **테스트 마킹**: 성능 계측 없이 즉시 각인기에 테스트 마킹 신호를 전송하는 버튼입니다.
+* *참고: `강제 검사 (스킵)` 및 `테스트 마킹` 버튼은 환경 설정에서 테스트 모드가 활성화된 상태에서만 화면 하단에 노출됩니다.*
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/04_통신_설정.md b/marking_gui/Manual/04_통신_설정.md
new file mode 100644
index 0000000..a3c30e5
--- /dev/null
+++ b/marking_gui/Manual/04_통신_설정.md
@@ -0,0 +1,37 @@
+# 4. 통신 설정 (Comm. Settings)
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 4.1 포트 설정 화면 사용법
+
+프로그램 상단의 `통신 설정` 버튼을 클릭하면 포트 설정 대화 상자가 나타납니다. 이 대화 상자에서 센서 판독 장치(4251 보드)의 연결 포트 및 채널을 설정할 수 있습니다.
+
+### 주요 설정 항목 및 설명
+
+1. **통신 포트 (Port)**:
+ - 센서 판독기가 연결되어 있는 PC의 연결 포트를 선택합니다. (예: `COM1`, `COM3`, `COM4` 등)
+ - 현재 PC에 인식되어 있는 포트 목록이 자동으로 표시됩니다.
+
+2. **통신 속도 (BaudRate)**:
+ - 센서 판독 장치와의 데이터 송수신 속도를 설정합니다.
+ - 기본 설정값은 **`115200`** 이며, 특수한 경우가 아니면 변경하지 않는 것을 권장합니다.
+
+3. **통신 채널 (Channel)**:
+ - 센서 판독기 내부의 연결 채널을 결정합니다.
+ - 드롭다운 메뉴를 통해 **`CH1`** 또는 **`CH2`**를 선택할 수 있습니다.
+ - 선택된 채널에 따라 센서 인식 모드가 변경됩니다.
+
+---
+
+## 4.2 설정 저장
+
+* **설정 저장**:
+ - 통신 포트와 채널을 선택한 후 `저장` 버튼을 누르면 설정 정보가 저장됩니다.
+ - 저장 시 기존 센서 연결을 해제하고 새로운 설정값으로 센서 인식 서비스를 다시 시작합니다. 프로그램을 재시작할 필요 없이 변경 사항이 즉시 적용됩니다.
+ - 저장하지 않고 창을 닫으려면 `취소` 버튼을 누릅니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/05_시험_설정.md b/marking_gui/Manual/05_시험_설정.md
new file mode 100644
index 0000000..e1a6849
--- /dev/null
+++ b/marking_gui/Manual/05_시험_설정.md
@@ -0,0 +1,44 @@
+# 5. 시험 설정 (Parameters)
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 5.1 판정 기준 설정
+
+프로그램 상단 우측의 `시험 설정` 버튼을 클릭하면 판정 기준을 구성하는 대화 상자가 나타납니다. 이 화면에서 제품 합불 판정에 사용되는 전압, 전류, 저항의 기준값을 입력할 수 있습니다.
+
+### 5.1.1 각 항목별 한계치 입력 가이드
+
+* **전압 판정 범위**:
+ * **하한값**: 합격 전압 하한 규격 (V 단위)
+ * **상한값**: 합격 전압 상한 규격 (V 단위)
+* **전류 판정 범위**:
+ * **하한값**: 합격 전류 하한 규격 (설정창 표기 단위 참조, 기본 mA/A)
+ * **상한값**: 합격 전류 상한 규격 (설정창 표기 단위 참조, 기본 mA/A)
+* **저항 판정 범위**:
+ * **하한값**: 합격 저항 하한 규격 (설정창 표기 단위 참조, 기본 Ω/kΩ)
+ * **상한값**: 합격 저항 상한 규격 (설정창 표기 단위 참조, 기본 Ω/kΩ)
+
+*참고: 저장 버튼을 클릭하면 메인 화면의 하단 가이드 문구(`기준범위: X ~ Y`)가 즉시 갱신되어 새로 입력한 한계치가 다음 검사 시퀀스부터 반영됩니다.*
+
+---
+
+## 5.2 레이저 각인 도면 지정
+
+* **각인 파일 설정 경로**:
+ * 레이저 마킹기에서 사용할 작업 도면 파일 선택은 설정 대화 상자가 아닌, **메인 화면 좌측 상단의 '작업 모델 설정' 영역에 있는 '파일 선택' 버튼**을 클릭하여 수행합니다.
+ * 도면이 선택되면 상단 헤더 배지의 모델명 정보도 해당 도면 파일명으로 자동으로 변경됩니다.
+* **유의 사항**:
+ * 지정해 놓은 도면 파일이 실제 경로에 존재하지 않는 경우, 계측 성능 검사가 합격하더라도 각인 단계에서 에러가 납니다. 검사 전 파일 경로 상태를 확인하십시오.
+
+---
+
+## 5.3 설정 저장 및 적용
+
+* 설정 창의 `저장` 버튼을 클릭하면 변경된 파라미터가 시스템 설정에 저장됩니다.
+* 저장 완료 즉시 **메인 화면 하단에 표시되는 판정 기준 가이드 텍스트**가 업데이트되며, 이후에 새로 시작하는 성능 검사 시퀀스부터 새로운 규격치가 적용됩니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/06_데이터_조회.md b/marking_gui/Manual/06_데이터_조회.md
new file mode 100644
index 0000000..3f29c42
--- /dev/null
+++ b/marking_gui/Manual/06_데이터_조회.md
@@ -0,0 +1,53 @@
+# 6. 측정 데이터 조회 및 데이터베이스 연동
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 6.1 선행 공정 결과 조회 (이력 확인)
+
+본 프로그램은 검사 시작 시 입력된 제품 일련번호를 기반으로 생산 데이터베이스에 접속하여 이전 제조 공정의 합격 여부를 조회합니다.
+
+### 6.1.1 이력 검토 대상 공정
+
+시스템은 다음 3개의 선행 공정 검사 결과를 확인합니다.
+1. **하우징 조립 공정**
+2. **센서 오차 보정 공정**
+3. **1차 EOL 성능 검사**
+
+### 6.1.2 이전 단계 규격 교차 확인 동작
+* 이전 3개 선행 단계의 이력이 **모두 합격(PASS 또는 OK)**된 제품인 경우에만 최종 성능검사(EOL2)를 진행할 수 있습니다.
+* 선행 이력이 존재하지 않거나, 단 한 곳이라도 불합격(FAIL 또는 NG) 판정을 받은 경우 화면 진행 상황 표시창에 경고가 표시되며 최종 성능검사 시작이 제한됩니다.
+
+---
+
+## 6.2 검사 결과 자동 적재 및 최종 판정 기록
+
+자동 계측 프로세스가 종료되면 다음 정보가 생산 데이터베이스에 저장됩니다.
+
+* **저장 정보**:
+ - 제품 일련번호
+ - 생산 라인, 로트 번호, 작업자 정보
+ - 실측 전압, 전류, 저항값
+ - 실측값은 데이터베이스 저장 규격에 맞춰 시스템 내부에서 소수점 이하 자리로 정밀 정리되어 저장됩니다.
+ - 최종 판정 결과 (`PASS` / `FAIL`)
+ - 마킹 시퀀스 번호 및 성공 기록
+* **비동기 데이터 처리**:
+ - 데이터베이스 통신 대기로 인한 화면 멈춤 현상을 방지하기 위해 데이터 조회 및 저장 작업은 프로그램 화면 동작과 별개로 백그라운드에서 비동기식으로 안전하게 처리됩니다.
+
+---
+
+## 6.3 오프라인 모드 운용
+
+데이터베이스 서버 접속이 불가능한 환경에서도 단독으로 설비를 가동할 수 있도록 **오프라인 모드**를 지원합니다.
+
+* **활성화 목적**:
+ - 네트워크 연결이 끊긴 상태에서 프로그램의 단독 동작 및 장비 제어 상태를 확인하기 위함입니다.
+* **오프라인 작동 패턴**:
+ - **공정 조회 우회**: 입력된 일련번호에 대해 생산 데이터베이스 서버 접속 없이 이전 공정을 합격한 것으로 우회 처리합니다.
+ - **임시 일련번호 생성**: 일련번호가 비어 있는 상태에서 테스트를 개시하는 경우 현재 시간 기반의 임시 일련번호가 자동으로 부여되어 진행합니다.
+ - **각인 모사**: 실제 각인기에 데이터를 전송하는 단계를 가상 성공 처리하여 전체 흐름을 테스트합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/07_시험_프로세스.md b/marking_gui/Manual/07_시험_프로세스.md
new file mode 100644
index 0000000..ea7abb0
--- /dev/null
+++ b/marking_gui/Manual/07_시험_프로세스.md
@@ -0,0 +1,75 @@
+# 7. 자동 시험 프로세스 및 15단계 시퀀스
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 7.1 자동 시험 및 마킹 프로세스 개요
+
+본 프로그램은 사용자가 메인 화면 하단의 `검사 시작 (START)` 버튼을 누르면 데이터 저장까지의 과정이 자동 계측 시퀀스에 따라 순차적으로 동작하도록 구성되어 있습니다.
+
+```mermaid
+graph TD
+ A[START 버튼 클릭] --> B[UI 화면 수치 초기화]
+ B --> C[4~8단계: 센서 S/N 읽기 및 보정]
+ C --> D{선행 공정 이력 확인}
+ D -- 불합격/미진행 --> E[검사 중단 & 경고 팝업]
+ E --> F{강제 검사 스킵?}
+ F -- 취소 --> Z[대기 복구]
+ F -- 스킵 실행 --> G[9~15단계: 계측 시퀀스 기동]
+ D -- 3개 공정 모두 PASS --> G
+ G --> H[10단계: 테스트 주전원 ON]
+ H --> I[12~14단계: 전압 / 저항 / 전류 실측]
+ I --> J[15단계: 테스트 주전원 안전 차단]
+ J --> K{최종 규격 비교 판정}
+ K -- 1개 이상 이탈 FAIL --> L[대형 FAIL 표시 & 마킹 제외 & DB 불합격 적재]
+ K -- 모두 통과 PASS --> M[대형 PASS 표시 & DB 합격 적재 & LMC1 레이저 QR 각인]
+ L --> Z
+ M --> Z
+```
+
+---
+
+## 7.2 상세 15단계 자동 제어 시퀀스
+
+최종 검사가 시작되면 장비와 계측기들은 다음 15가지 단계에 따라 동작을 차례대로 수행합니다.
+
+### [1단계] 사전 전원 인가 단계 (최초 가동/로그인 시 1회 진행)
+* **1단계 [장비 확인]**: 전원 공급 장치와의 통신 연결 상태를 점검합니다.
+* **2단계 [센서 구동 전압 설정]**: 센서 보드를 구동하는 데 필요한 전압을 전원 장치에 설정합니다.
+* **3단계 [센서 구동 전원 출력]**: 설정된 전원을 출력하여 보드를 부팅합니다.
+* *(참고: 이 단계는 로그인 성공 시 1회만 실행되며, 이후의 반복 검사 과정에서는 이미 투입된 전원을 재사용하여 4단계부터 즉각 검사가 수행됩니다)*
+
+### [2단계] 제품 일련번호 수집 및 센서 통신 대기 단계
+* **4단계 [판독 장치 대기]**: 센서 판독 장치를 초기 대기 상태로 설정합니다.
+* **5단계 [보드 통신 연결]**: 통신 설정에 명시된 채널을 선택하여 센서 보드와 연결합니다.
+* **6단계 [일련번호 읽기]**: 제품 메모리로부터 스캔한 고유 일련번호를 로드합니다.
+* **7단계 [보드 통신 해제]**: 센서 데이터 전송을 중지하고 대기 상태로 되돌립니다.
+* **8단계 [보정 기준 전송]**: 센서 장치에 보정 기준값을 인계하고 동작을 종료합니다.
+
+### [3단계] 계측기 연동 성능 평가 및 전원 차단 단계
+* **9단계 [전원 장비 확인]**: 주 테스트 전원을 공급할 전원 장치의 통신 상태를 확인합니다.
+* **10단계 [테스트 전원 인가]**: 테스트 전원 출력을 ON시키고, 전원 인가 후 신호 안정을 위해 잠시 대기합니다.
+* **11단계 [측정 장비 확인]**: 측정 장치의 통신 상태를 확인합니다.
+* **12단계 [전압 측정]**: 제품의 현재 작동 전압을 측정합니다.
+* **13단계 [저항 측정]**: 제품의 전기적 저항값을 직접 실측합니다.
+* **14단계 [전류 측정]**: 전원 공급 장치의 출력 채널을 통해 제품의 작동 소모 전류를 측정합니다.
+* **15단계 [테스트 전원 차단]**: 수집 완료 후 테스트 전원 출력을 자동으로 차단하여 제품을 안전하게 보호합니다. (계측 도중 오류가 발생하더라도 강제로 전원이 차단되도록 안전 장치가 적용되어 있습니다.)
+
+---
+
+## 7.3 최종 판정 및 마킹 실행 조건
+
+계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.
+
+1. **기준 규격을 벗어난 경우 (FAIL)**:
+ * 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다.
+ * 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
+2. **모든 항목이 합격인 경우 (PASS)**:
+ * 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
+ * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
+ * 마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/08_로그_관리.md b/marking_gui/Manual/08_로그_관리.md
new file mode 100644
index 0000000..2bea961
--- /dev/null
+++ b/marking_gui/Manual/08_로그_관리.md
@@ -0,0 +1,39 @@
+# 8. 로그 및 데이터 관리
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 8.1 시스템 로그 수집 및 관리
+
+본 제어 프로그램은 설비 가동 중에 발생하는 계측기와의 통신 상태, 측정값 정보 및 각종 예외 오류 상황을 로그 파일로 기록합니다.
+
+### 8.1.1 로그 파일 보관 방법
+
+* **보관 폴더**: 프로그램 실행 파일 경로의 하위 **`Logs`** 폴더에 저장됩니다.
+* **파일명 규칙**: 날짜별로 파일이 자동으로 생성되어 보관됩니다.
+* **로그 중요도 분류**:
+ * **정보 (INFO)**: 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록.
+ * **경고 (WARN)**: 센서 판독기 인식 지연, 제품 한계 기준치를 벗어난 불합격(FAIL) 발생 기록 등 설비 동작은 유지되나 품질 확인이 필요한 정보.
+ * **오류 (ERROR)**: 데이터베이스 네트워크 접속 끊김, 마킹 장치 연결 실패 등 조치가 필요한 장비 오류 정보.
+
+---
+
+## 8.2 장비 교체 및 프로그램 백업 방법
+
+장비를 교환하거나 PC 복구 상황에 대비하여, 정상 작동 상태에서의 설정 파일들을 정기적으로 백업해 둘 것을 권장합니다.
+
+### 8.2.1 백업 대상 리스트
+
+1. **계측기 설정 및 검사 기준값 백업**:
+ * 계측기들의 연결 설정 정보와 합격 판정 기준 범위가 저장됩니다.
+2. **데이터베이스 서버 정보 백업**:
+ * 생산 정보 데이터베이스 서버 주소와 로그인 정보가 저장됩니다.
+3. **센서 채널 정보 백업**:
+ * 일련번호 판독 통신 포트 번호와 설정 채널 정보가 저장됩니다.
+4. **레이저 마킹 도면 파일 백업**:
+ * 제품 표면에 각인할 마킹 도면 파일입니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/09_오류_대응.md b/marking_gui/Manual/09_오류_대응.md
new file mode 100644
index 0000000..7af8b77
--- /dev/null
+++ b/marking_gui/Manual/09_오류_대응.md
@@ -0,0 +1,59 @@
+# 9. 오류 대응 및 프로그램 종료
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 9.1 현장 장애 유형별 점검 및 예외 조치 요령
+
+공정 가동 중 장비 상태나 전산망에 이상이 생길 경우 발생할 수 있는 주요 화면 경고창 및 조치 요령입니다.
+
+### 9.1.1 [오류 1] "제품 ID(IC_SN)를 읽어오지 못했습니다"
+* **현상**: 검사 시작 시 제품 일련번호 읽기 단계에서 경고창이 발생하고 검사가 취소됨.
+* **원인 및 해결**:
+ 1. **접촉 상태 점검**: 제품이 안착 지그에 완벽하게 안착되었는지 확인하고, 프레스 실린더 또는 핀 단자의 밀착 상태를 확인하십시오.
+ 2. **통신 설정 점검**: 프로그램 상단의 `통신 설정` 창을 열어 현재 설정된 포트 번호 및 물리 채널 번호가 실제 케이블 연결 상태와 일치하는지 확인하십시오.
+
+### 9.1.2 [오류 2] "이전 공정 이력이 조회되지 않는 제품입니다"
+* **현상**: 검사 시작 시 이전 단계 결과가 조회되지 않아 검사가 진행되지 않음.
+* **원인 및 해결**:
+ - 해당 제품이 하우징 조립, 센서 보정, EOL 1차 성능 검사 등 이전 가공 공정을 진행하지 않았거나, 이전 설비에서 생산 DB에 데이터를 올바르게 전송하지 않은 상태입니다. 제품 바코드를 재확인하고 이전 설비의 데이터 전송 유무를 검토하십시오.
+* **비상 조치 (강제 검사)**:
+ - 이전 이력 등록 상태와 무관하게 **제품 성능 계측 및 마킹을 실행하고 결과를 저장해야 하는 특수 테스트 상황**인 경우, 화면 최하단의 **`강제 검사 (스킵)`** 버튼을 누르면 DB 검증을 건너뛰고 성능 검사를 강제로 실행할 수 있습니다.
+
+### 9.1.3 [오류 3] "이전 공정 단계가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다"
+* **현상**: DB 조회 결과 이전 공정 중 불합격(FAIL 또는 NG) 판정을 받은 내역이 발견되어 검사가 중단됨.
+* **원인 및 해결**:
+ - 이전 공정에서 불합격 판정을 받은 제품은 최종 평가 및 레이저 마킹을 가동할 수 없습니다. 불량 제품 격리 기준에 따라 해당 제품을 생산 라인에서 제외 조치하십시오.
+* **비상 조치 (강제 검사)**:
+ - 설비 작동 확인이나 품질 분석 등을 위해 반드시 검사를 진행해야 하는 경우, 하단바의 **`강제 검사 (스킵)`** 버튼을 클릭하면 경고 상태를 건너뛰고 계측과 마킹을 실행할 수 있습니다.
+
+### 9.1.4 [오류 4] "레이저 각인 작업 전송 실패"
+* **현상**: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함.
+* **원인 및 해결**:
+ 1. **도면 파일 점검**: `시험 설정` 화면에서 세팅해 놓은 작업 도면 파일이 실제 해당 경로에 존재하는지 점검하십시오.
+ 2. **마킹기 전원/연결 점검**: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오.
+
+### 9.1.5 [오류 5] "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오."
+* **현상**:
+ - 프로그램 실행 또는 로그인 완료 후, 메인 화면 상태 알림창에 빨간색 경고 배너로 문구가 표시됩니다.
+ - `검사 시작 (START)` 또는 `강제 검사 (스킵)` 버튼을 누르면 작동 차단 경고창이 나타나며 설비 검사가 시작되지 않습니다.
+* **원인 및 해결**:
+ - **순번 범위 초과**: 현재 로트 번호(`Lot No`)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다.
+ - **해결 방법**: 새로운 생산 조 또는 로트 번호(`Lot No`)를 수정하여 **프로그램에 재로그인**하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다.
+
+---
+
+## 9.2 프로그램 정상 종료 및 전원 자동 차단 규칙
+
+본 설비는 고전력 계측 및 전원 제어를 함께 수행하므로 대기 전력 소모 방지와 안전을 위해 아래 절차에 따라 정상 종료 프로세스를 밟아야 합니다.
+
+* **로그아웃 (Logout)**:
+ - 상단 메뉴의 `로그아웃` 버튼을 클릭하면 현재 작업자 정보가 해제되고 초기 로그인 창으로 전환됩니다.
+ - **이와 동시에 연동된 전원 공급 장치로 차단 신호가 자동으로 전송되어 주전원 및 보드 부팅용 상시전원 출력이 OFF 상태로 차단됩니다.**
+* **프로그램 완전 종료**:
+ - 화면 우측 상단의 `닫기(X)` 버튼을 눌러 프로그램을 최종 종료할 때도, 자동으로 상시전원 차단 명령을 장비에 인계한 뒤 레이저 각인 드라이버 리소스를 닫고 메모리를 환원합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Manual/Manual_Merged.md b/marking_gui/Manual/Manual_Merged.md
new file mode 100644
index 0000000..c63c0d4
--- /dev/null
+++ b/marking_gui/Manual/Manual_Merged.md
@@ -0,0 +1,844 @@
+
+
+# Performance Inspect & Laser Marking System - 사용자 메뉴얼
+
+> **버전**: 1.0.0 | **최종 수정**: 2026-07-14
+
+---
+
+## 메뉴얼 목차
+
+본 메뉴얼은 각 주제별로 별도 문서로 구성되어 있습니다. 아래 링크를 클릭하여 해당 문서로 이동하세요.
+
+| 번호 | 문서 | 설명 |
+|------|------|------|
+| 1 | [시스템 개요 및 요구사항](#1-시스템-개요-및-요구사항) | 프로그램 소개, 주요 기능, 하드웨어/소프트웨어 요구사항, 장비 구성 |
+| 2 | [초기 설정 가이드](#2-초기-설정-가이드) | 설정 파일의 구성 요소 안내 및 최초 장비 가동 시 점검 순서 |
+| 3 | [메인 화면 (Home View)](#3-메인-화면-home-view) | 상단 정보 표시, 제품 정보 카드, 검사 결과 화면, 진행 상황 확인 및 버튼 제어 가이드 |
+| 4 | [통신 설정 (Comm. Settings)](#4-통신-설정-comm-settings) | 4251 센서 통신 채널 설정 및 PC 통신 포트 연동 방법 |
+| 5 | [시험 설정 (Parameters)](#5-시험-설정-parameters) | 제품 규격치(전압, 전류, 저항 범위) 및 레이저 마킹 도면 파일 연동 |
+| 6 | [측정 데이터 조회 (Data View)](#6-측정-데이터-조회-및-데이터베이스-연동) | 생산 데이터베이스를 통한 이전 공정 이력 자동 조회 및 최종 결과 저장 원리 |
+| 7 | [자동 시험 프로세스](#7-자동-시험-프로세스-및-14단계-시퀀스) | 자동 계측 시퀀스 기동, 마킹 수행 및 최종 합격 판정 흐름 |
+| 8 | [로그 및 데이터 관리](#8-로그-및-데이터-관리) | 시스템 로그 파일 확인 방법 및 장비 설정 파일 백업 방법 |
+| 9 | [오류 대응 및 프로그램 종료](#9-오류-대응-및-프로그램-종료) | 센서 통신 오류, DB 조회 에러, 마킹 불량 등 현장 장애 대응 및 안전한 프로그램 종료 방법 |
+
+---
+
+## 빠른 시작 가이드 (Quick Start)
+
+처음 시스템을 가동하여 검사를 진행하는 경우 다음 순서에 따라 작업을 수행해 주십시오.
+
+1. **프로그램 설치 (최초 1회)**
+ - 배포받은 단일 설치 프로그램 `MarkingSetup.exe`를 실행하여 클릭 한 번으로 `C:\marking` 경로 및 바탕화면 바로가기를 자동 생성합니다.
+2. **하드웨어 연결 상태 확인**
+ - 계측 장비들과 제어용 PC 간의 네트워크(LAN)선 및 센서 리더용 시리얼 포트 케이블 연결을 확인합니다.
+3. **프로그램 실행 및 로그인**
+ - 바탕화면의 `Marking System` (`marking_gui`) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.)
+3. **통신 상태 확인 및 채널 설정**
+ - 우측 상단의 `통신 설정` 메뉴에서 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.
+4. **검사 규격값 검증**
+ - 우측 상단의 `시험 설정` 메뉴를 열고, 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.
+5. **성능 검사 및 마킹 시작**
+ - 안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 `검사 시작 (START)` 버튼을 눌러 계측 및 마킹을 수행합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 1. 시스템 개요 및 요구사항
+
+
+---
+
+## 1.1 시스템 개요
+
+**Performance Inspect & Laser Marking System**은 온압센서 완제품의 최종 성능 검사를 수행하고, 이전 공정 단계들의 합격(PASS) 이력을 조회하여 최종 적합 제품에 한해 제품 표면에 레이저 마킹을 수행하는 시스템 제어 프로그램입니다.
+
+본 프로그램은 측정 계측기와 센서 판독기, 레이저 각인 장치 및 공장 생산 데이터베이스와 연동되어 작동합니다.
+
+### 주요 기능 요약
+
+| 기능 | 설명 |
+|------|------|
+| **일련번호(S/N) 자동 추출** | 센서 판독 보드를 통해 제품 안착 시 제품 내부 메모리로부터 일련번호를 추출합니다. |
+| **선행 공정 검사 교차 조회** | 이전 단계(하우징 조립, 센서 보정, 1차 EOL 성능 검사)가 모두 합격 판정되었는지 생산 데이터베이스에서 조회합니다. |
+| **자동 계측 시퀀스** | 전원 공급 장치와 전압/저항 측정 장비들과 연동하여 제품에 테스트 전원을 투입하고 전압, 저항, 전류 값을 차례대로 계측합니다. |
+| **실제 저항값 측정** | 전압/저항 DMM 측정기를 통해 센서의 실제 저항값(Ω)을 측정합니다. |
+| **전류 측정** | 전원 공급 장치(Power Supply)의 출력 채널 1을 통해 제품의 소모 전류를 계측합니다. |
+| **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 2D QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. |
+| **결과 기록** | 계측 판정 규격(전압, 전류, 저항)과 마킹 성공 여부를 최종 판정하여 생산 데이터베이스에 저장합니다. |
+
+---
+
+## 1.2 소프트웨어 요구사항
+
+| 항목 | 요구사항 | 비고 |
+|------|----------|------|
+| **통합 설치 프로그램** | `MarkingSetup.exe` (WiX 번들) | 1-Click 자동 설치 및 실행 환경, 드라이버, 설정 일체 셋업 |
+| **운영체제** | Windows 10 이상 | 프로그램 구동 및 장비 드라이버 운용용 |
+| **데이터베이스** | Microsoft SQL Server (MS-SQL) | 생산 공정 데이터 및 계측 결과 연동용 |
+| **레이저 제어 프로그램** | EZCAD 각인 제어 프로그램 및 드라이버 | 레이저 마킹기 구동 및 전용 도면(.ezd) 운용용 |
+| **네트워크 환경** | LAN (TCP/IP) 통신 | 제어 PC와 측정 기기 간의 연결 |
+| **센서 보드 통신** | COM 포트 (RS-232 시리얼) | 4251 센서 통신 보드 연동 |
+
+---
+
+## 1.3 하드웨어 구성
+
+| 장비 | 역할 | 통신 방식 |
+|------|------|-----------|
+| **센서 판독 보드 (4251 보드)** | 제품 내부 칩의 일련번호(IC_SN)를 인식합니다. | RS-232 시리얼 통신 |
+| **전압 및 저항 측정 장비 (DMM)** | 제품의 전압(V) 및 전기적 저항(Ω)을 측정합니다. (예: 192.168.1.100) | TCP/IP 네트워크 통신 |
+| **전류 측정 및 전원 공급 장비 (PSU)** | 제품 구동을 위한 전원 공급 및 소모 전류(A)를 측정합니다. (예: 192.168.1.101) | TCP/IP 네트워크 통신 |
+| **레이저 마킹기** | 2D QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 |
+| *(전류 측정 DMM)* | *설정 파일(예: 192.168.1.102)에 존재하나 현재 제어 시퀀스에서는 사용하지 않습니다.* | *(미사용)* |
+
+## 1.4 설치 경로(`C:\marking\`) 구조 및 생성 파일 역할
+
+단일 통합 설치 마법사(`MarkingSetup.exe`)를 통해 시스템을 설치하면 기본 설치 디렉터리(`C:\marking\`) 아래에 다음과 같은 실행 바이너리, 설정 파일, 드라이버 라이브러리 및 런타임 로그 폴더가 배치·생성됩니다.
+
+| 구분 | 파일 / 폴더명 | 상세 역할 및 기능 설명 |
+|------|----------------|------------------------|
+| **실행 프로그램** | `marking_gui.exe` | 메인 성능 검사 및 레이저 마킹 GUI 제어 실행 파일 |
+| **종속 라이브러리** | `marking_gui.dll`, `*.deps.json`, `*.runtimeconfig.json` | .NET 9.0 (WPF) 자립형 런타임 및 종속 구성 바이너리 |
+| **통합 설정 파일** | `Setting.ini` | 마킹기 폴더 경로, 테스트 모드, 도안 태그 매핑, 계측기 LAN IP, SCPI 전원 커맨드, 계측 판정 규격 임계치(`[Limits]`) 종합 설정 파일 |
+| **DB 설정 파일** | `Database.ini` | MS-SQL 데이터베이스 접속 주소, DB명, 사용자 계정/PW, 오프라인 모드(`OfflineMode=true`) 설정 파일 |
+| **센서 설정 파일** | `Sensor.ini` | 4251 센서 통신 보드 시리얼 포트(`Port=COM3`), 통신 속도(`BaudRate=115200`), 통신 채널(`Channel=1`) 설정 파일 |
+| **드라이버 폴더** | `libs/` | C++ LMC 각인 드라이버 격리 디렉터리 (`MarkEzdWrapper.dll`, `DataMgr.dll`, `MarkEzd.dll`, `haspms32.dll` 등) |
+| **로그 저장 폴더** | `logs/` *(자동 생성)* | 시스템 운용 중 발생하는 일별 이벤트 로그, 통신 이력, 각인 성공/실패 기록 텍스트 저장 디렉터리 (예: `logs/2026-07-23.log`) |
+| **백업 보관 폴더** | `publish_backup/` | 프로그램 업데이트 및 빌드 배포 시 이전 릴리즈 바이너리/설정 자동 백업 폴더 |
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 2. 초기 설정 가이드
+
+
+---
+
+## 2.1 프로그램 설정 구성 개요
+
+본 프로그램은 정상적인 계측기 통신 및 데이터베이스 연동을 위해 몇 가지 설정 파일을 사용합니다. 이 설정 파일들은 프로그램 설치 및 라인 도입 시 최초 1회 셋업을 원칙으로 하며, 일반 운영자가 직접 설정 파일을 열어 수정할 필요는 없습니다.
+
+*참고: 대부분의 검사 기준 및 통신 정보는 프로그램 내의 **'통신 설정'** 및 **'시험 설정'** 화면을 통해 수정할 수 있습니다.*
+
+### 2.1.1 주요 설정 정보 구분
+
+1. **장비 및 계측 설정**
+ - 계측기들과의 네트워크 연결 주소와 대기 시간 등을 정의합니다.
+ - 제품 합격 여부를 판정하는 전압, 전류, 저항의 기본 합격 규격 범위가 이 파일에 저장되며, 화면 내의 **'시험 설정'** 창에서 수정한 사항이 이 파일에 동적으로 반영됩니다.
+ - 프로그램은 전압/저항 측정 장치와 전원/전류 공급 장치를 제어하여 계측을 진행합니다.
+
+2. **생산 데이터베이스 연동 설정**
+ - 공장 생산 관리 시스템 데이터베이스와의 통신 경로가 기록되어 있습니다.
+ - 전산망이나 DB 서버 점검 등으로 인해 통신이 어려울 경우, 설정 파일을 통해 **'오프라인 모드'**로 전환하여 단독으로 장비 작동 테스트를 해볼 수 있습니다.
+ - **오프라인 로그인 지원**: 데이터베이스 확인 단계 없이 로그인하여 프로그램을 기동합니다.
+ - **오프라인 검사 지원**: 제품 이력 조회 및 검사 결과 저장을 건너뛰고 단독으로 계측 및 마킹을 실행합니다.
+
+3. **센서 판독기 설정**
+ - 제품 일련번호를 로드하기 위한 통신 채널 및 연결 포트 주소가 정의됩니다.
+ - 화면 내의 **'통신 설정'** 창에서 수정한 사항이 이 파일에 자동으로 저장됩니다.
+
+### 2.1.2 설치 경로(`C:\marking\`) 내 파일 배치 구조
+
+프로그램 설치 완료 후 `C:\marking\` 경로에는 다음과 같이 구동 파일, 설정 파일, 드라이버 라이브러리 및 런타임 자동 생성 디렉터리가 배치됩니다.
+
+* **`C:\marking\marking_gui.exe`**: GUI 제어 메인 실행 파일
+* **`C:\marking\Setting.ini`**: 장비 통신 IP, 마킹 태그, 규격 임계치(`[Limits]`) 종합 설정 파일
+* **`C:\marking\Database.ini`**: MS-SQL DB 접속 정보 및 오프라인 모드 설정 파일
+* **`C:\marking\Sensor.ini`**: 4251 센서 통신 보드 포트/속도/채널 설정 파일
+* **`C:\marking\libs\`**: C++ LMC 레이저 각인 드라이버 라이브러리 격리 폴더 (`MarkEzdWrapper.dll` 등)
+* **`C:\marking\logs\`**: 런타임 가동 중 자동 생성되는 일별 이력 로그 폴더 (`yyyy-MM-dd.log`)
+* **`C:\marking\publish_backup\`**: 업데이트/배포 시 이전 버전 바이너리 및 설정 자동 백업 폴더
+
+### 2.1.3 기본적인 설정 파일 편집 방법
+
+> [!IMPORTANT]
+> **[권장 조치 안내]**
+> 현장 라인 검사자 및 일반 작업자는 프로그램 UI의 **[통신 설정]** 및 **[시험 설정]** 메뉴를 통해 안전하게 파라미터를 변경하십시오.
+> 아래의 `.ini` 파일 직접 수정은 라인 엔지니어 또는 설비 관리자 전용 권한 작업이며, 잘못된 파일 수정 시 장비 통신 오류가 발생할 수 있습니다.
+
+설비 이동이나 네트워크 환경 변경 시, 프로그램 폴더 내의 각 `.ini` 파일을 텍스트 편집기(메모장 등)로 열어 아래의 전체 설정 레이아웃 항목을 참조하여 수정할 수 있습니다.
+
+#### 1) Setting.ini (주요 변경 가능 설정)
+현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다.
+```ini
+[Settings]
+EzdFolderPath=c:\EzCad\EasyCfg ; EZD 도안 파일 기본 탐색/저장 폴더 경로
+test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false)
+
+[DMM]
+VoltageHost=192.168.1.100 ; 전압/저항 측정용 계측기(DMM) IP 주소
+
+[CurrentDMM]
+CurrentDMMHost=192.168.1.102 ; 전류 측정용 계측기(DMM) IP 주소
+
+[PowerSupply]
+CurrentHost=192.168.1.101 ; 전원 공급기(Power Supply) IP 주소
+
+[Limits]
+MinVoltage=4.0 ; 전압 하한 판정 기준값 (V)
+MaxVoltage=6.0 ; 전압 상한 판정 기준값 (V)
+MinCurrent=8.0 ; 전류 하한 판정 기준값 (mA)
+MaxCurrent=10.0 ; 전류 상한 판정 기준값 (mA)
+MinResistance=0.4 ; 저항 하한 판정 기준값 (kΩ)
+MaxResistance=0.8 ; 저항 상한 판정 기준값 (kΩ)
+```
+
+#### 2) Database.ini (주요 변경 가능 설정)
+DB 서버 접속 주소 및 점검용 오프라인 모드 변경 시 수정하는 항목입니다.
+```ini
+[Database]
+Db_Server=192.168.1.200 ; MSSQL 데이터베이스 서버 IP 주소
+Db_Name=MarkingDb ; 연결할 데이터베이스 이름
+Db_User=db_user ; DB 접속 사용자 계정
+Db_Password=db_password ; DB 접속 비밀번호
+OfflineMode=False ; DB 미연결 점검 모드 (True: DB 저장 생략, False: 정상 검증/적재)
+
+[Login]
+OfflinePreview=False ; 로그인 DB 검증 우회 여부 (True: 로그인 생략, False: 정상 검증)
+```
+
+#### 3) Sensor.ini (주요 변경 가능 설정)
+바코드 스캐너 시리얼 포트 및 채널 설정 항목입니다.
+```ini
+[Sensor]
+Port=COM3 ; 바코드 스캐너 시리얼 포트 (장치 관리자 COM 포트 번호)
+Channel=1 ; 통신 채널 번호 (1 또는 2)
+```
+
+---
+
+## 2.2 최초 실행 및 장비 점검 순서
+
+라인 가동 전 최초 실행할 때는 아래 단계를 순서대로 이행하여 정상 작동 여부를 점검하십시오.
+
+1. **프로그램 단일 설치 (MarkingSetup.exe)**
+ - 배포받은 단일 설치 마법사 `MarkingSetup.exe`를 실행하여 1-Click으로 `C:\marking\` 설치 및 바탕화면 바로가기를 자동 생성합니다.
+2. **데이터베이스 연결 상태 확인**
+ - 데이터베이스가 점검 중인 경우 설정 파일을 통해 오프라인 모드로 설정하면 로그인 창을 통과하여 단독 테스트를 진행할 수 있습니다.
+3. **장비 네트워크 통신 점검**
+ - 제어 PC와 계측 장비들(전압/저항 측정 장비, 전원 공급 장치) 간의 LAN 케이블 연결을 확인합니다.
+ - 제어 PC의 네트워크 IP 환경이 장비들의 대역과 동일한지 확인하고, 통신 신호가 정상적으로 송수신되는지 점검합니다.
+4. **상시전원 자동 인가 확인**
+ - 프로그램 실행 후 작업자 계정으로 로그인을 시도합니다.
+ - 로그인이 완료되면 전원 공급 장치의 상시전원 채널 전원 램프가 자동으로 켜지는지 확인합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 3. 메인 화면 (Home View)
+
+
+---
+
+## 3.1 메인 화면 레이아웃 구성
+
+본 프로그램은 작업자가 화면 구성 및 각 항목 상태를 파악할 수 있도록 4개의 카드 레이아웃으로 화면을 구성했습니다.
+
+### 3.1.1 상단 헤더 영역 (Header Bar)
+
+프로그램 최상단 영역에는 시스템 정보 배지와 제어 버튼이 배치되어 있습니다.
+* **실시간 시계**: 좌측 상단에 날짜와 시간이 표시됩니다.
+* **작업 정보 배지**:
+ * **공정 라인**: 로그인 단계에서 지정한 생산 라인 코드 (`Line No`)
+ * **로트 번호**: 로그인 단계에서 지정한 생산 로트 번호 (`Lot No`)
+ * **모델명**: 현재 가동 중인 제품의 사양 모델 (`Model`)
+ * **작업자**: 로그인한 작업자의 이름 (`Operator`)
+* **제어 버튼**: 우측 상단에 `로그아웃` 버튼이 위치하며 최하단 바에 설정 관련 버튼이 배치됩니다.
+
+---
+
+## 3.2 상태 및 제어 카드 영역
+
+화면 중앙에는 역할을 구분한 4개의 상태 카드가 배치되어 있어, 제품 상태와 계측 단계를 모니터링할 수 있습니다.
+
+### 3.2.1 [카드 1] 제품 정보 및 이전 결과 (좌측 상단)
+제품의 일련번호와 이전 생산 공정에서의 합격 이력을 대조하는 영역입니다.
+* **작업 모델 설정**:
+ - `파일 선택` 버튼을 사용하여 각인용 도면 파일을 불러옵니다.
+ - 도면 파일 선택이 완료되면 상단 헤더 영역의 모델명(Model) 배지 텍스트가 선택한 도면명으로 자동 변경됩니다.
+* **일련번호 (IC_SN)**: 제품이 안착되면 센서 판독기를 통해 자동으로 인식되어 표시됩니다. (읽기 전용)
+* **이전 공정 이력 배지**:
+ * **Housing**: 하우징 조립 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
+ * **Calibration**: 압력 보정 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
+ * **EOL**: EOL 1차 성능 평가 판정 상태 (`PASS` / `FAIL` / `대기`)
+ * *주의: 이전 공정 이력 배지 중 하나라도 `FAIL` 상태인 경우 성능 검사를 시작할 수 없습니다.*
+
+### 3.2.2 [카드 2] 측정 결과 (우측 상단)
+현재 장비에서 실제 계측한 3대 전기 파라미터(전압, 전류, 저항) 수치와 설정된 기준 규격 범위를 표시하는 영역입니다.
+* **측정 전압 (Voltage)**: 전압 측정 장치가 측정한 전압 수치 (**V** 단위)
+* **측정 전류 (Current)**: 전원 공급 장치가 측정한 소모 전류 수치 (**mA** 단위)
+* **측정 저항 (Resistance)**: 저항 측정 장치가 측정한 전기적 저항 수치 (**kΩ** 단위)
+* *알림: 각 물리량의 하단에는 현재 적용 중인 기준 규격 범위가 문구(`기준범위: X ~ Y`)로 표출되며, 실측값이 규격 범위 내에 도달하면 카드 테두리가 **초록색(PASS)**으로 표시되고, 범위를 벗어나면 테두리가 **빨간색(FAIL)**으로 바뀝니다.*
+
+### 3.2.3 [카드 3] 진행상황 (좌측 하단)
+현재 설비가 수행하고 있는 단계별 자동 제어 시퀀스를 보여주는 안내 창입니다.
+* "대기 상태", "제품 정보 수집 중...", "이전 공정 검사 결과 조회 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 작동 상태를 실시간 텍스트로 표출합니다.
+
+### 3.2.4 [카드 4] 판정결과 (우측 하단)
+제품의 최종 합격(PASS) 및 불합격(FAIL) 상태를 표시합니다.
+* 계측 검사와 데이터 저장, 레이저 각인이 완료되었을 때 최종 합격인 경우 카드 배경이 **초록색(PASS)**으로 전환되며, 불합격인 경우 카드 배경이 **빨간색(FAIL)**으로 변경됩니다.
+
+### 3.2.5 최하단 제어 영역 (Control Box)
+* **검사 시작 (START)**: 전체 검사 시퀀스를 기동하는 메인 버튼입니다. 이 버튼을 누르면 이전 계측 수치들이 초기화되고 새로운 측정 프로세스가 개시됩니다.
+* **강제 검사 (스킵)**: 이전 가공 공정의 합격 이력이 없더라도 검증 단계를 건너뛰고 성능 계측 및 각인 시퀀스를 기동하는 버튼입니다.
+* **테스트 마킹**: 성능 계측 없이 즉시 각인기에 테스트 마킹 신호를 전송하는 버튼입니다.
+* *참고: `강제 검사 (스킵)` 및 `테스트 마킹` 버튼은 환경 설정에서 테스트 모드가 활성화된 상태에서만 화면 하단에 노출됩니다.*
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 4. 통신 설정 (Comm. Settings)
+
+
+---
+
+## 4.1 포트 설정 화면 사용법
+
+프로그램 상단의 `통신 설정` 버튼을 클릭하면 포트 설정 대화 상자가 나타납니다. 이 대화 상자에서 센서 판독 장치(4251 보드)의 연결 포트 및 채널을 설정할 수 있습니다.
+
+### 주요 설정 항목 및 설명
+
+1. **통신 포트 (Port)**:
+ - 센서 판독기가 연결되어 있는 PC의 연결 포트를 선택합니다. (예: `COM1`, `COM3`, `COM4` 등)
+ - 현재 PC에 인식되어 있는 포트 목록이 자동으로 표시됩니다.
+
+2. **통신 속도 (BaudRate)**:
+ - 센서 판독 장치와의 데이터 송수신 속도를 설정합니다.
+ - 기본 설정값은 **`115200`** 이며, 특수한 경우가 아니면 변경하지 않는 것을 권장합니다.
+
+3. **통신 채널 (Channel)**:
+ - 센서 판독기 내부의 연결 채널을 결정합니다.
+ - 드롭다운 메뉴를 통해 **`CH1`** 또는 **`CH2`**를 선택할 수 있습니다.
+ - 선택된 채널에 따라 센서 인식 모드가 변경됩니다.
+
+---
+
+## 4.2 설정 저장
+
+* **설정 저장**:
+ - 통신 포트와 채널을 선택한 후 `저장` 버튼을 누르면 설정 정보가 저장됩니다.
+ - 저장 시 기존 센서 연결을 해제하고 새로운 설정값으로 센서 인식 서비스를 다시 시작합니다. 프로그램을 재시작할 필요 없이 변경 사항이 즉시 적용됩니다.
+ - 저장하지 않고 창을 닫으려면 `취소` 버튼을 누릅니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 5. 시험 설정 (Parameters)
+
+
+---
+
+## 5.1 판정 기준 설정
+
+프로그램 상단 우측의 `시험 설정` 버튼을 클릭하면 판정 기준을 구성하는 대화 상자가 나타납니다. 이 화면에서 제품 합불 판정에 사용되는 전압, 전류, 저항의 기준값을 입력할 수 있습니다.
+
+### 5.1.1 각 항목별 한계치 입력 가이드
+
+* **전압 판정 범위**:
+ * **하한값**: 합격 전압 하한 규격 (V 단위)
+ * **상한값**: 합격 전압 상한 규격 (V 단위)
+* **전류 판정 범위**:
+ * **하한값**: 합격 전류 하한 규격 (설정창 표기 단위 참조, 기본 mA/A)
+ * **상한값**: 합격 전류 상한 규격 (설정창 표기 단위 참조, 기본 mA/A)
+* **저항 판정 범위**:
+ * **하한값**: 합격 저항 하한 규격 (설정창 표기 단위 참조, 기본 Ω/kΩ)
+ * **상한값**: 합격 저항 상한 규격 (설정창 표기 단위 참조, 기본 Ω/kΩ)
+
+*참고: 저장 버튼을 클릭하면 메인 화면의 하단 가이드 문구(`기준범위: X ~ Y`)가 즉시 갱신되어 새로 입력한 한계치가 다음 검사 시퀀스부터 반영됩니다.*
+
+---
+
+## 5.2 레이저 각인 도면 지정
+
+* **각인 파일 설정 경로**:
+ * 레이저 마킹기에서 사용할 작업 도면 파일 선택은 설정 대화 상자가 아닌, **메인 화면 좌측 상단의 '작업 모델 설정' 영역에 있는 '파일 선택' 버튼**을 클릭하여 수행합니다.
+ * 도면이 선택되면 상단 헤더 배지의 모델명 정보도 해당 도면 파일명으로 자동으로 변경됩니다.
+* **유의 사항**:
+ * 지정해 놓은 도면 파일이 실제 경로에 존재하지 않는 경우, 계측 성능 검사가 합격하더라도 각인 단계에서 에러가 납니다. 검사 전 파일 경로 상태를 확인하십시오.
+
+---
+
+## 5.3 설정 저장 및 적용
+
+* 설정 창의 `저장` 버튼을 클릭하면 변경된 파라미터가 시스템 설정에 저장됩니다.
+* 저장 완료 즉시 **메인 화면 하단에 표시되는 판정 기준 가이드 텍스트**가 업데이트되며, 이후에 새로 시작하는 성능 검사 시퀀스부터 새로운 규격치가 적용됩니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 6. 측정 데이터 조회 및 데이터베이스 연동
+
+
+---
+
+## 6.1 선행 공정 결과 조회 (이력 확인)
+
+본 프로그램은 검사 시작 시 입력된 제품 일련번호를 기반으로 생산 데이터베이스에 접속하여 이전 제조 공정의 합격 여부를 조회합니다.
+
+### 6.1.1 이력 검토 대상 공정
+
+시스템은 다음 3개의 선행 공정 검사 결과를 확인합니다.
+1. **하우징 조립 공정**
+2. **센서 오차 보정 공정**
+3. **1차 EOL 성능 검사**
+
+### 6.1.2 이전 단계 규격 교차 확인 동작
+* 이전 3개 선행 단계의 이력이 **모두 합격(PASS 또는 OK)**된 제품인 경우에만 최종 성능검사(EOL2)를 진행할 수 있습니다.
+* 선행 이력이 존재하지 않거나, 단 한 곳이라도 불합격(FAIL 또는 NG) 판정을 받은 경우 화면 진행 상황 표시창에 경고가 표시되며 최종 성능검사 시작이 제한됩니다.
+
+---
+
+## 6.2 검사 결과 자동 적재 및 최종 판정 기록
+
+자동 계측 프로세스가 종료되면 다음 정보가 생산 데이터베이스에 저장됩니다.
+
+* **저장 정보**:
+ - 제품 일련번호
+ - 생산 라인, 로트 번호, 작업자 정보
+ - 실측 전압, 전류, 저항값
+ - 실측값은 데이터베이스 저장 규격에 맞춰 시스템 내부에서 소수점 이하 자리로 정밀 정리되어 저장됩니다.
+ - 최종 판정 결과 (`PASS` / `FAIL`)
+ - 마킹 시퀀스 번호 및 성공 기록
+* **비동기 데이터 처리**:
+ - 데이터베이스 통신 대기로 인한 화면 멈춤 현상을 방지하기 위해 데이터 조회 및 저장 작업은 프로그램 화면 동작과 별개로 백그라운드에서 비동기식으로 안전하게 처리됩니다.
+
+---
+
+## 6.3 오프라인 모드 운용
+
+데이터베이스 서버 접속이 불가능한 환경에서도 단독으로 설비를 가동할 수 있도록 **오프라인 모드**를 지원합니다.
+
+* **활성화 목적**:
+ - 네트워크 연결이 끊긴 상태에서 프로그램의 단독 동작 및 장비 제어 상태를 확인하기 위함입니다.
+* **오프라인 작동 패턴**:
+ - **공정 조회 우회**: 입력된 일련번호에 대해 생산 데이터베이스 서버 접속 없이 이전 공정을 합격한 것으로 우회 처리합니다.
+ - **임시 일련번호 생성**: 일련번호가 비어 있는 상태에서 테스트를 개시하는 경우 현재 시간 기반의 임시 일련번호가 자동으로 부여되어 진행합니다.
+ - **각인 모사**: 실제 각인기에 데이터를 전송하는 단계를 가상 성공 처리하여 전체 흐름을 테스트합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 7. 자동 시험 프로세스 및 15단계 시퀀스
+
+
+---
+
+## 7.1 자동 시험 및 마킹 프로세스 개요
+
+본 프로그램은 사용자가 메인 화면 하단의 `검사 시작 (START)` 버튼을 누르면 데이터 저장까지의 과정이 자동 계측 시퀀스에 따라 순차적으로 동작하도록 구성되어 있습니다.
+
+```mermaid
+graph TD
+ A[START 버튼 클릭] --> B[UI 화면 수치 초기화]
+ B --> C[4~8단계: 센서 S/N 읽기 및 보정]
+ C --> D{선행 공정 이력 확인}
+ D -- 불합격/미진행 --> E[검사 중단 & 경고 팝업]
+ E --> F{강제 검사 스킵?}
+ F -- 취소 --> Z[대기 복구]
+ F -- 스킵 실행 --> G[9~15단계: 계측 시퀀스 기동]
+ D -- 3개 공정 모두 PASS --> G
+ G --> H[10단계: 테스트 주전원 ON]
+ H --> I[12~14단계: 전압 / 저항 / 전류 실측]
+ I --> J[15단계: 테스트 주전원 안전 차단]
+ J --> K{최종 규격 비교 판정}
+ K -- 1개 이상 이탈 FAIL --> L[대형 FAIL 표시 & 마킹 제외 & DB 불합격 적재]
+ K -- 모두 통과 PASS --> M[대형 PASS 표시 & DB 합격 적재 & LMC1 레이저 QR 각인]
+ L --> Z
+ M --> Z
+```
+
+---
+
+## 7.2 상세 15단계 자동 제어 시퀀스
+
+최종 검사가 시작되면 장비와 계측기들은 다음 15가지 단계에 따라 동작을 차례대로 수행합니다.
+
+### [1단계] 사전 전원 인가 단계 (최초 가동/로그인 시 1회 진행)
+* **1단계 [장비 확인]**: 전원 공급 장치와의 통신 연결 상태를 점검합니다.
+* **2단계 [센서 구동 전압 설정]**: 센서 보드를 구동하는 데 필요한 전압을 전원 장치에 설정합니다.
+* **3단계 [센서 구동 전원 출력]**: 설정된 전원을 출력하여 보드를 부팅합니다.
+* *(참고: 이 단계는 로그인 성공 시 1회만 실행되며, 이후의 반복 검사 과정에서는 이미 투입된 전원을 재사용하여 4단계부터 즉각 검사가 수행됩니다)*
+
+### [2단계] 제품 일련번호 수집 및 센서 통신 대기 단계
+* **4단계 [판독 장치 대기]**: 센서 판독 장치를 초기 대기 상태로 설정합니다.
+* **5단계 [보드 통신 연결]**: 통신 설정에 명시된 채널을 선택하여 센서 보드와 연결합니다.
+* **6단계 [일련번호 읽기]**: 제품 메모리로부터 스캔한 고유 일련번호를 로드합니다.
+* **7단계 [보드 통신 해제]**: 센서 데이터 전송을 중지하고 대기 상태로 되돌립니다.
+* **8단계 [보정 기준 전송]**: 센서 장치에 보정 기준값을 인계하고 동작을 종료합니다.
+
+### [3단계] 계측기 연동 성능 평가 및 전원 차단 단계
+* **9단계 [전원 장비 확인]**: 주 테스트 전원을 공급할 전원 장치의 통신 상태를 확인합니다.
+* **10단계 [테스트 전원 인가]**: 테스트 전원 출력을 ON시키고, 전원 인가 후 신호 안정을 위해 잠시 대기합니다.
+* **11단계 [측정 장비 확인]**: 측정 장치의 통신 상태를 확인합니다.
+* **12단계 [전압 측정]**: 제품의 현재 작동 전압을 측정합니다.
+* **13단계 [저항 측정]**: 제품의 전기적 저항값을 직접 실측합니다.
+* **14단계 [전류 측정]**: 전원 공급 장치의 출력 채널을 통해 제품의 작동 소모 전류를 측정합니다.
+* **15단계 [테스트 전원 차단]**: 수집 완료 후 테스트 전원 출력을 자동으로 차단하여 제품을 안전하게 보호합니다. (계측 도중 오류가 발생하더라도 강제로 전원이 차단되도록 안전 장치가 적용되어 있습니다.)
+
+---
+
+## 7.3 최종 판정 및 마킹 실행 조건
+
+계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.
+
+1. **기준 규격을 벗어난 경우 (FAIL)**:
+ * 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다.
+ * 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
+2. **모든 항목이 합격인 경우 (PASS)**:
+ * 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
+ * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
+ * 마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 8. 로그 및 데이터 관리
+
+
+---
+
+## 8.1 시스템 로그 수집 및 관리
+
+본 제어 프로그램은 설비 가동 중에 발생하는 계측기와의 통신 상태, 측정값 정보 및 각종 예외 오류 상황을 로그 파일로 기록합니다.
+
+### 8.1.1 로그 파일 보관 방법
+
+* **보관 폴더**: 프로그램 실행 파일 경로의 하위 **`Logs`** 폴더에 저장됩니다.
+* **파일명 규칙**: 날짜별로 파일이 자동으로 생성되어 보관됩니다.
+* **로그 중요도 분류**:
+ * **정보 (INFO)**: 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록.
+ * **경고 (WARN)**: 센서 판독기 인식 지연, 제품 한계 기준치를 벗어난 불합격(FAIL) 발생 기록 등 설비 동작은 유지되나 품질 확인이 필요한 정보.
+ * **오류 (ERROR)**: 데이터베이스 네트워크 접속 끊김, 마킹 장치 연결 실패 등 조치가 필요한 장비 오류 정보.
+
+---
+
+## 8.2 장비 교체 및 프로그램 백업 방법
+
+장비를 교환하거나 PC 복구 상황에 대비하여, 정상 작동 상태에서의 설정 파일들을 정기적으로 백업해 둘 것을 권장합니다.
+
+### 8.2.1 백업 대상 리스트
+
+1. **계측기 설정 및 검사 기준값 백업**:
+ * 계측기들의 연결 설정 정보와 합격 판정 기준 범위가 저장됩니다.
+2. **데이터베이스 서버 정보 백업**:
+ * 생산 정보 데이터베이스 서버 주소와 로그인 정보가 저장됩니다.
+3. **센서 채널 정보 백업**:
+ * 일련번호 판독 통신 포트 번호와 설정 채널 정보가 저장됩니다.
+4. **레이저 마킹 도면 파일 백업**:
+ * 제품 표면에 각인할 마킹 도면 파일입니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
+
+
+# 9. 오류 대응 및 프로그램 종료
+
+
+---
+
+## 9.1 현장 장애 유형별 점검 및 예외 조치 요령
+
+공정 가동 중 장비 상태나 전산망에 이상이 생길 경우 발생할 수 있는 주요 화면 경고창 및 조치 요령입니다.
+
+### 9.1.1 [오류 1] "제품 ID(IC_SN)를 읽어오지 못했습니다"
+* **현상**: 검사 시작 시 제품 일련번호 읽기 단계에서 경고창이 발생하고 검사가 취소됨.
+* **원인 및 해결**:
+ 1. **접촉 상태 점검**: 제품이 안착 지그에 완벽하게 안착되었는지 확인하고, 프레스 실린더 또는 핀 단자의 밀착 상태를 확인하십시오.
+ 2. **통신 설정 점검**: 프로그램 상단의 `통신 설정` 창을 열어 현재 설정된 포트 번호 및 물리 채널 번호가 실제 케이블 연결 상태와 일치하는지 확인하십시오.
+
+### 9.1.2 [오류 2] "이전 공정 이력이 조회되지 않는 제품입니다"
+* **현상**: 검사 시작 시 이전 단계 결과가 조회되지 않아 검사가 진행되지 않음.
+* **원인 및 해결**:
+ - 해당 제품이 하우징 조립, 센서 보정, EOL 1차 성능 검사 등 이전 가공 공정을 진행하지 않았거나, 이전 설비에서 생산 DB에 데이터를 올바르게 전송하지 않은 상태입니다. 제품 바코드를 재확인하고 이전 설비의 데이터 전송 유무를 검토하십시오.
+* **비상 조치 (강제 검사)**:
+ - 이전 이력 등록 상태와 무관하게 **제품 성능 계측 및 마킹을 실행하고 결과를 저장해야 하는 특수 테스트 상황**인 경우, 화면 최하단의 **`강제 검사 (스킵)`** 버튼을 누르면 DB 검증을 건너뛰고 성능 검사를 강제로 실행할 수 있습니다.
+
+### 9.1.3 [오류 3] "이전 공정 단계가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다"
+* **현상**: DB 조회 결과 이전 공정 중 불합격(FAIL 또는 NG) 판정을 받은 내역이 발견되어 검사가 중단됨.
+* **원인 및 해결**:
+ - 이전 공정에서 불합격 판정을 받은 제품은 최종 평가 및 레이저 마킹을 가동할 수 없습니다. 불량 제품 격리 기준에 따라 해당 제품을 생산 라인에서 제외 조치하십시오.
+* **비상 조치 (강제 검사)**:
+ - 설비 작동 확인이나 품질 분석 등을 위해 반드시 검사를 진행해야 하는 경우, 하단바의 **`강제 검사 (스킵)`** 버튼을 클릭하면 경고 상태를 건너뛰고 계측과 마킹을 실행할 수 있습니다.
+
+### 9.1.4 [오류 4] "레이저 각인 작업 전송 실패"
+* **현상**: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함.
+* **원인 및 해결**:
+ 1. **도면 파일 점검**: `시험 설정` 화면에서 세팅해 놓은 작업 도면 파일이 실제 해당 경로에 존재하는지 점검하십시오.
+ 2. **마킹기 전원/연결 점검**: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오.
+
+### 9.1.5 [오류 5] "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오."
+* **현상**:
+ - 프로그램 실행 또는 로그인 완료 후, 메인 화면 상태 알림창에 빨간색 경고 배너로 문구가 표시됩니다.
+ - `검사 시작 (START)` 또는 `강제 검사 (스킵)` 버튼을 누르면 작동 차단 경고창이 나타나며 설비 검사가 시작되지 않습니다.
+* **원인 및 해결**:
+ - **순번 범위 초과**: 현재 로트 번호(`Lot No`)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다.
+ - **해결 방법**: 새로운 생산 조 또는 로트 번호(`Lot No`)를 수정하여 **프로그램에 재로그인**하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다.
+
+---
+
+## 9.2 프로그램 정상 종료 및 전원 자동 차단 규칙
+
+본 설비는 고전력 계측 및 전원 제어를 함께 수행하므로 대기 전력 소모 방지와 안전을 위해 아래 절차에 따라 정상 종료 프로세스를 밟아야 합니다.
+
+* **로그아웃 (Logout)**:
+ - 상단 메뉴의 `로그아웃` 버튼을 클릭하면 현재 작업자 정보가 해제되고 초기 로그인 창으로 전환됩니다.
+ - **이와 동시에 연동된 전원 공급 장치로 차단 신호가 자동으로 전송되어 주전원 및 보드 부팅용 상시전원 출력이 OFF 상태로 차단됩니다.**
+* **프로그램 완전 종료**:
+ - 화면 우측 상단의 `닫기(X)` 버튼을 눌러 프로그램을 최종 종료할 때도, 자동으로 상시전원 차단 명령을 장비에 인계한 뒤 레이저 각인 드라이버 리소스를 닫고 메모리를 환원합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
+
+
diff --git a/marking_gui/Manual/Manual_for_pdf.html b/marking_gui/Manual/Manual_for_pdf.html
new file mode 100644
index 0000000..5d6621d
--- /dev/null
+++ b/marking_gui/Manual/Manual_for_pdf.html
@@ -0,0 +1,974 @@
+
+
+
+
+ Performance Inspect & Laser Marking System - 사용자 메뉴얼
+
+
+
+
Performance Inspect & Laser Marking System - 사용자 메뉴얼
+
+
버전: 1.0.0 | 최종 수정: 2026-07-14
+
+
+
메뉴얼 목차
+
본 메뉴얼은 각 주제별로 별도 문서로 구성되어 있습니다. 아래 링크를 클릭하여 해당 문서로 이동하세요.
마킹기 폴더 경로, 테스트 모드, 도안 태그 매핑, 계측기 LAN IP, SCPI 전원 커맨드, 계측 판정 규격 임계치([Limits]) 종합 설정 파일
+
+
+
DB 설정 파일
+
Database.ini
+
MS-SQL 데이터베이스 접속 주소, DB명, 사용자 계정/PW, 오프라인 모드(OfflineMode=true) 설정 파일
+
+
+
센서 설정 파일
+
Sensor.ini
+
4251 센서 통신 보드 시리얼 포트(Port=COM3), 통신 속도(BaudRate=115200), 통신 채널(Channel=1) 설정 파일
+
+
+
드라이버 폴더
+
libs/
+
C++ LMC 각인 드라이버 격리 디렉터리 (MarkEzdWrapper.dll, DataMgr.dll, MarkEzd.dll, haspms32.dll 등)
+
+
+
로그 저장 폴더
+
logs/(자동 생성)
+
시스템 운용 중 발생하는 일별 이벤트 로그, 통신 이력, 각인 성공/실패 기록 텍스트 저장 디렉터리 (예: logs/2026-07-23.log)
+
+
+
백업 보관 폴더
+
publish_backup/
+
프로그램 업데이트 및 빌드 배포 시 이전 릴리즈 바이너리/설정 자동 백업 폴더
+
+
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
2. 초기 설정 가이드
+
+
2.1 프로그램 설정 구성 개요
+
본 프로그램은 정상적인 계측기 통신 및 데이터베이스 연동을 위해 몇 가지 설정 파일을 사용합니다. 이 설정 파일들은 프로그램 설치 및 라인 도입 시 최초 1회 셋업을 원칙으로 하며, 일반 운영자가 직접 설정 파일을 열어 수정할 필요는 없습니다.
+
참고: 대부분의 검사 기준 및 통신 정보는 프로그램 내의 '통신 설정' 및 '시험 설정' 화면을 통해 수정할 수 있습니다.
+
2.1.1 주요 설정 정보 구분
+
+
장비 및 계측 설정
+
계측기들과의 네트워크 연결 주소와 대기 시간 등을 정의합니다.
+
제품 합격 여부를 판정하는 전압, 전류, 저항의 기본 합격 규격 범위가 이 파일에 저장되며, 화면 내의 '시험 설정' 창에서 수정한 사항이 이 파일에 동적으로 반영됩니다.
+
+
프로그램은 전압/저항 측정 장치와 전원/전류 공급 장치를 제어하여 계측을 진행합니다.
+
+
+
생산 데이터베이스 연동 설정
+
+
공장 생산 관리 시스템 데이터베이스와의 통신 경로가 기록되어 있습니다.
+
+
전산망이나 DB 서버 점검 등으로 인해 통신이 어려울 경우, 설정 파일을 통해 '오프라인 모드'로 전환하여 단독으로 장비 작동 테스트를 해볼 수 있습니다.
+
+
오프라인 로그인 지원: 데이터베이스 확인 단계 없이 로그인하여 프로그램을 기동합니다.
+
오프라인 검사 지원: 제품 이력 조회 및 검사 결과 저장을 건너뛰고 단독으로 계측 및 마킹을 실행합니다.
+
+
+
+
센서 판독기 설정
+
+
제품 일련번호를 로드하기 위한 통신 채널 및 연결 포트 주소가 정의됩니다.
+
화면 내의 '통신 설정' 창에서 수정한 사항이 이 파일에 자동으로 저장됩니다.
+
+
2.1.2 설치 경로(C:\marking\) 내 파일 배치 구조
+
프로그램 설치 완료 후 C:\marking\ 경로에는 다음과 같이 구동 파일, 설정 파일, 드라이버 라이브러리 및 런타임 자동 생성 디렉터리가 배치됩니다.
+
+
C:\marking\marking_gui.exe: GUI 제어 메인 실행 파일
+
C:\marking\Setting.ini: 장비 통신 IP, 마킹 태그, 규격 임계치([Limits]) 종합 설정 파일
+
C:\marking\Database.ini: MS-SQL DB 접속 정보 및 오프라인 모드 설정 파일
+
C:\marking\Sensor.ini: 4251 센서 통신 보드 포트/속도/채널 설정 파일
+
C:\marking\libs\: C++ LMC 레이저 각인 드라이버 라이브러리 격리 폴더 (MarkEzdWrapper.dll 등)
+
C:\marking\logs\: 런타임 가동 중 자동 생성되는 일별 이력 로그 폴더 (yyyy-MM-dd.log)
+
C:\marking\publish_backup\: 업데이트/배포 시 이전 버전 바이너리 및 설정 자동 백업 폴더
+
+
2.1.3 기본적인 설정 파일 편집 방법
+
+
[!IMPORTANT]
+[권장 조치 안내]
+현장 라인 검사자 및 일반 작업자는 프로그램 UI의 [통신 설정] 및 [시험 설정] 메뉴를 통해 안전하게 파라미터를 변경하십시오.
+아래의 .ini 파일 직접 수정은 라인 엔지니어 또는 설비 관리자 전용 권한 작업이며, 잘못된 파일 수정 시 장비 통신 오류가 발생할 수 있습니다.
+
+
설비 이동이나 네트워크 환경 변경 시, 프로그램 폴더 내의 각 .ini 파일을 텍스트 편집기(메모장 등)로 열어 아래의 전체 설정 레이아웃 항목을 참조하여 수정할 수 있습니다.
+
1) Setting.ini (주요 변경 가능 설정)
+
현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다.
+
[Settings]
+EzdFolderPath=c:\EzCad\EasyCfg ; EZD 도안 파일 기본 탐색/저장 폴더 경로
+test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false)
+
+[DMM]
+VoltageHost=192.168.1.100 ; 전압/저항 측정용 계측기(DMM) IP 주소
+
+[CurrentDMM]
+CurrentDMMHost=192.168.1.102 ; 전류 측정용 계측기(DMM) IP 주소
+
+[PowerSupply]
+CurrentHost=192.168.1.101 ; 전원 공급기(Power Supply) IP 주소
+
+[Limits]
+MinVoltage=4.0 ; 전압 하한 판정 기준값 (V)
+MaxVoltage=6.0 ; 전압 상한 판정 기준값 (V)
+MinCurrent=8.0 ; 전류 하한 판정 기준값 (mA)
+MaxCurrent=10.0 ; 전류 상한 판정 기준값 (mA)
+MinResistance=0.4 ; 저항 하한 판정 기준값 (kΩ)
+MaxResistance=0.8 ; 저항 상한 판정 기준값 (kΩ)
+
+
2) Database.ini (주요 변경 가능 설정)
+
DB 서버 접속 주소 및 점검용 오프라인 모드 변경 시 수정하는 항목입니다.
+
[Database]
+Db_Server=192.168.1.200 ; MSSQL 데이터베이스 서버 IP 주소
+Db_Name=MarkingDb ; 연결할 데이터베이스 이름
+Db_User=db_user ; DB 접속 사용자 계정
+Db_Password=db_password ; DB 접속 비밀번호
+OfflineMode=False ; DB 미연결 점검 모드 (True: DB 저장 생략, False: 정상 검증/적재)
+
+[Login]
+OfflinePreview=False ; 로그인 DB 검증 우회 여부 (True: 로그인 생략, False: 정상 검증)
+
+
3) Sensor.ini (주요 변경 가능 설정)
+
바코드 스캐너 시리얼 포트 및 채널 설정 항목입니다.
+
[Sensor]
+Port=COM3 ; 바코드 스캐너 시리얼 포트 (장치 관리자 COM 포트 번호)
+Channel=1 ; 통신 채널 번호 (1 또는 2)
+
+
+
2.2 최초 실행 및 장비 점검 순서
+
라인 가동 전 최초 실행할 때는 아래 단계를 순서대로 이행하여 정상 작동 여부를 점검하십시오.
+
+
프로그램 단일 설치 (MarkingSetup.exe)
+
배포받은 단일 설치 마법사 MarkingSetup.exe를 실행하여 1-Click으로 C:\marking\ 설치 및 바탕화면 바로가기를 자동 생성합니다.
+
데이터베이스 연결 상태 확인
+
데이터베이스가 점검 중인 경우 설정 파일을 통해 오프라인 모드로 설정하면 로그인 창을 통과하여 단독 테스트를 진행할 수 있습니다.
+
장비 네트워크 통신 점검
+
제어 PC와 계측 장비들(전압/저항 측정 장비, 전원 공급 장치) 간의 LAN 케이블 연결을 확인합니다.
+
제어 PC의 네트워크 IP 환경이 장비들의 대역과 동일한지 확인하고, 통신 신호가 정상적으로 송수신되는지 점검합니다.
+
상시전원 자동 인가 확인
+
프로그램 실행 후 작업자 계정으로 로그인을 시도합니다.
+
로그인이 완료되면 전원 공급 장치의 상시전원 채널 전원 램프가 자동으로 켜지는지 확인합니다.
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
3. 메인 화면 (Home View)
+
+
3.1 메인 화면 레이아웃 구성
+
본 프로그램은 작업자가 화면 구성 및 각 항목 상태를 파악할 수 있도록 4개의 카드 레이아웃으로 화면을 구성했습니다.
+
3.1.1 상단 헤더 영역 (Header Bar)
+
프로그램 최상단 영역에는 시스템 정보 배지와 제어 버튼이 배치되어 있습니다.
+* 실시간 시계: 좌측 상단에 날짜와 시간이 표시됩니다.
+* 작업 정보 배지:
+ * 공정 라인: 로그인 단계에서 지정한 생산 라인 코드 (Line No)
+ * 로트 번호: 로그인 단계에서 지정한 생산 로트 번호 (Lot No)
+ * 모델명: 현재 가동 중인 제품의 사양 모델 (Model)
+ * 작업자: 로그인한 작업자의 이름 (Operator)
+* 제어 버튼: 우측 상단에 로그아웃 버튼이 위치하며 최하단 바에 설정 관련 버튼이 배치됩니다.
+
+
3.2 상태 및 제어 카드 영역
+
화면 중앙에는 역할을 구분한 4개의 상태 카드가 배치되어 있어, 제품 상태와 계측 단계를 모니터링할 수 있습니다.
+
3.2.1 [카드 1] 제품 정보 및 이전 결과 (좌측 상단)
+
제품의 일련번호와 이전 생산 공정에서의 합격 이력을 대조하는 영역입니다.
+* 작업 모델 설정:
+ - 파일 선택 버튼을 사용하여 각인용 도면 파일을 불러옵니다.
+ - 도면 파일 선택이 완료되면 상단 헤더 영역의 모델명(Model) 배지 텍스트가 선택한 도면명으로 자동 변경됩니다.
+* 일련번호 (IC_SN): 제품이 안착되면 센서 판독기를 통해 자동으로 인식되어 표시됩니다. (읽기 전용)
+* 이전 공정 이력 배지:
+ * Housing: 하우징 조립 공정 판정 상태 (PASS / FAIL / 대기)
+ * Calibration: 압력 보정 공정 판정 상태 (PASS / FAIL / 대기)
+ * EOL: EOL 1차 성능 평가 판정 상태 (PASS / FAIL / 대기)
+ * 주의: 이전 공정 이력 배지 중 하나라도 FAIL 상태인 경우 성능 검사를 시작할 수 없습니다.
+
3.2.2 [카드 2] 측정 결과 (우측 상단)
+
현재 장비에서 실제 계측한 3대 전기 파라미터(전압, 전류, 저항) 수치와 설정된 기준 규격 범위를 표시하는 영역입니다.
+* 측정 전압 (Voltage): 전압 측정 장치가 측정한 전압 수치 (V 단위)
+* 측정 전류 (Current): 전원 공급 장치가 측정한 소모 전류 수치 (mA 단위)
+* 측정 저항 (Resistance): 저항 측정 장치가 측정한 전기적 저항 수치 (kΩ 단위)
+* 알림: 각 물리량의 하단에는 현재 적용 중인 기준 규격 범위가 문구(기준범위: X ~ Y)로 표출되며, 실측값이 규격 범위 내에 도달하면 카드 테두리가 초록색(PASS)으로 표시되고, 범위를 벗어나면 테두리가 빨간색(FAIL)으로 바뀝니다.
+
3.2.3 [카드 3] 진행상황 (좌측 하단)
+
현재 설비가 수행하고 있는 단계별 자동 제어 시퀀스를 보여주는 안내 창입니다.
+* "대기 상태", "제품 정보 수집 중...", "이전 공정 검사 결과 조회 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 작동 상태를 실시간 텍스트로 표출합니다.
+
3.2.4 [카드 4] 판정결과 (우측 하단)
+
제품의 최종 합격(PASS) 및 불합격(FAIL) 상태를 표시합니다.
+* 계측 검사와 데이터 저장, 레이저 각인이 완료되었을 때 최종 합격인 경우 카드 배경이 초록색(PASS)으로 전환되며, 불합격인 경우 카드 배경이 빨간색(FAIL)으로 변경됩니다.
+
3.2.5 최하단 제어 영역 (Control Box)
+
+
검사 시작 (START): 전체 검사 시퀀스를 기동하는 메인 버튼입니다. 이 버튼을 누르면 이전 계측 수치들이 초기화되고 새로운 측정 프로세스가 개시됩니다.
+
강제 검사 (스킵): 이전 가공 공정의 합격 이력이 없더라도 검증 단계를 건너뛰고 성능 계측 및 각인 시퀀스를 기동하는 버튼입니다.
+
테스트 마킹: 성능 계측 없이 즉시 각인기에 테스트 마킹 신호를 전송하는 버튼입니다.
+
참고: 강제 검사 (스킵) 및 테스트 마킹 버튼은 환경 설정에서 테스트 모드가 활성화된 상태에서만 화면 하단에 노출됩니다.
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
4. 통신 설정 (Comm. Settings)
+
+
4.1 포트 설정 화면 사용법
+
프로그램 상단의 통신 설정 버튼을 클릭하면 포트 설정 대화 상자가 나타납니다. 이 대화 상자에서 센서 판독 장치(4251 보드)의 연결 포트 및 채널을 설정할 수 있습니다.
+
주요 설정 항목 및 설명
+
+
통신 포트 (Port):
+
센서 판독기가 연결되어 있는 PC의 연결 포트를 선택합니다. (예: COM1, COM3, COM4 등)
+
+
현재 PC에 인식되어 있는 포트 목록이 자동으로 표시됩니다.
+
+
+
통신 속도 (BaudRate):
+
+
센서 판독 장치와의 데이터 송수신 속도를 설정합니다.
+
+
기본 설정값은 115200 이며, 특수한 경우가 아니면 변경하지 않는 것을 권장합니다.
+
+
+
통신 채널 (Channel):
+
+
센서 판독기 내부의 연결 채널을 결정합니다.
+
드롭다운 메뉴를 통해 CH1 또는 CH2를 선택할 수 있습니다.
+
선택된 채널에 따라 센서 인식 모드가 변경됩니다.
+
+
+
4.2 설정 저장
+
+
설정 저장:
+
통신 포트와 채널을 선택한 후 저장 버튼을 누르면 설정 정보가 저장됩니다.
+
저장 시 기존 센서 연결을 해제하고 새로운 설정값으로 센서 인식 서비스를 다시 시작합니다. 프로그램을 재시작할 필요 없이 변경 사항이 즉시 적용됩니다.
+
저장하지 않고 창을 닫으려면 취소 버튼을 누릅니다.
+
+
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
5. 시험 설정 (Parameters)
+
+
5.1 판정 기준 설정
+
프로그램 상단 우측의 시험 설정 버튼을 클릭하면 판정 기준을 구성하는 대화 상자가 나타납니다. 이 화면에서 제품 합불 판정에 사용되는 전압, 전류, 저항의 기준값을 입력할 수 있습니다.
+
5.1.1 각 항목별 한계치 입력 가이드
+
+
전압 판정 범위:
+
하한값: 합격 전압 하한 규격 (V 단위)
+
상한값: 합격 전압 상한 규격 (V 단위)
+
+
+
전류 판정 범위:
+
하한값: 합격 전류 하한 규격 (설정창 표기 단위 참조, 기본 mA/A)
+
상한값: 합격 전류 상한 규격 (설정창 표기 단위 참조, 기본 mA/A)
+
+
+
저항 판정 범위:
+
하한값: 합격 저항 하한 규격 (설정창 표기 단위 참조, 기본 Ω/kΩ)
+
상한값: 합격 저항 상한 규격 (설정창 표기 단위 참조, 기본 Ω/kΩ)
+
+
+
+
참고: 저장 버튼을 클릭하면 메인 화면의 하단 가이드 문구(기준범위: X ~ Y)가 즉시 갱신되어 새로 입력한 한계치가 다음 검사 시퀀스부터 반영됩니다.
+
+
5.2 레이저 각인 도면 지정
+
+
각인 파일 설정 경로:
+
레이저 마킹기에서 사용할 작업 도면 파일 선택은 설정 대화 상자가 아닌, 메인 화면 좌측 상단의 '작업 모델 설정' 영역에 있는 '파일 선택' 버튼을 클릭하여 수행합니다.
+
도면이 선택되면 상단 헤더 배지의 모델명 정보도 해당 도면 파일명으로 자동으로 변경됩니다.
+
+
+
유의 사항:
+
지정해 놓은 도면 파일이 실제 경로에 존재하지 않는 경우, 계측 성능 검사가 합격하더라도 각인 단계에서 에러가 납니다. 검사 전 파일 경로 상태를 확인하십시오.
+
+
+
+
+
5.3 설정 저장 및 적용
+
+
설정 창의 저장 버튼을 클릭하면 변경된 파라미터가 시스템 설정에 저장됩니다.
+
저장 완료 즉시 메인 화면 하단에 표시되는 판정 기준 가이드 텍스트가 업데이트되며, 이후에 새로 시작하는 성능 검사 시퀀스부터 새로운 규격치가 적용됩니다.
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
6. 측정 데이터 조회 및 데이터베이스 연동
+
+
6.1 선행 공정 결과 조회 (이력 확인)
+
본 프로그램은 검사 시작 시 입력된 제품 일련번호를 기반으로 생산 데이터베이스에 접속하여 이전 제조 공정의 합격 여부를 조회합니다.
+
6.1.1 이력 검토 대상 공정
+
시스템은 다음 3개의 선행 공정 검사 결과를 확인합니다.
+1. 하우징 조립 공정
+2. 센서 오차 보정 공정
+3. 1차 EOL 성능 검사
+
6.1.2 이전 단계 규격 교차 확인 동작
+
+
이전 3개 선행 단계의 이력이 모두 합격(PASS 또는 OK)된 제품인 경우에만 최종 성능검사(EOL2)를 진행할 수 있습니다.
+
선행 이력이 존재하지 않거나, 단 한 곳이라도 불합격(FAIL 또는 NG) 판정을 받은 경우 화면 진행 상황 표시창에 경고가 표시되며 최종 성능검사 시작이 제한됩니다.
+
+
+
6.2 검사 결과 자동 적재 및 최종 판정 기록
+
자동 계측 프로세스가 종료되면 다음 정보가 생산 데이터베이스에 저장됩니다.
+
+
저장 정보:
+
제품 일련번호
+
생산 라인, 로트 번호, 작업자 정보
+
실측 전압, 전류, 저항값
+
실측값은 데이터베이스 저장 규격에 맞춰 시스템 내부에서 소수점 이하 자리로 정밀 정리되어 저장됩니다.
+
+
+
최종 판정 결과 (PASS / FAIL)
+
마킹 시퀀스 번호 및 성공 기록
+
+
+
비동기 데이터 처리:
+
데이터베이스 통신 대기로 인한 화면 멈춤 현상을 방지하기 위해 데이터 조회 및 저장 작업은 프로그램 화면 동작과 별개로 백그라운드에서 비동기식으로 안전하게 처리됩니다.
+
+
+
+
+
6.3 오프라인 모드 운용
+
데이터베이스 서버 접속이 불가능한 환경에서도 단독으로 설비를 가동할 수 있도록 오프라인 모드를 지원합니다.
+
+
활성화 목적:
+
네트워크 연결이 끊긴 상태에서 프로그램의 단독 동작 및 장비 제어 상태를 확인하기 위함입니다.
+
+
+
오프라인 작동 패턴:
+
공정 조회 우회: 입력된 일련번호에 대해 생산 데이터베이스 서버 접속 없이 이전 공정을 합격한 것으로 우회 처리합니다.
+
임시 일련번호 생성: 일련번호가 비어 있는 상태에서 테스트를 개시하는 경우 현재 시간 기반의 임시 일련번호가 자동으로 부여되어 진행합니다.
+
각인 모사: 실제 각인기에 데이터를 전송하는 단계를 가상 성공 처리하여 전체 흐름을 테스트합니다.
+
+
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
7. 자동 시험 프로세스 및 15단계 시퀀스
+
+
7.1 자동 시험 및 마킹 프로세스 개요
+
본 프로그램은 사용자가 메인 화면 하단의 검사 시작 (START) 버튼을 누르면 데이터 저장까지의 과정이 자동 계측 시퀀스에 따라 순차적으로 동작하도록 구성되어 있습니다.
+
graph TD
+ A[START 버튼 클릭] --> B[UI 화면 수치 초기화]
+ B --> C[4~8단계: 센서 S/N 읽기 및 보정]
+ C --> D{선행 공정 이력 확인}
+ D -- 불합격/미진행 --> E[검사 중단 & 경고 팝업]
+ E --> F{강제 검사 스킵?}
+ F -- 취소 --> Z[대기 복구]
+ F -- 스킵 실행 --> G[9~15단계: 계측 시퀀스 기동]
+ D -- 3개 공정 모두 PASS --> G
+ G --> H[10단계: 테스트 주전원 ON]
+ H --> I[12~14단계: 전압 / 저항 / 전류 실측]
+ I --> J[15단계: 테스트 주전원 안전 차단]
+ J --> K{최종 규격 비교 판정}
+ K -- 1개 이상 이탈 FAIL --> L[대형 FAIL 표시 & 마킹 제외 & DB 불합격 적재]
+ K -- 모두 통과 PASS --> M[대형 PASS 표시 & DB 합격 적재 & LMC1 레이저 QR 각인]
+ L --> Z
+ M --> Z
+
+
+
7.2 상세 15단계 자동 제어 시퀀스
+
최종 검사가 시작되면 장비와 계측기들은 다음 15가지 단계에 따라 동작을 차례대로 수행합니다.
+
[1단계] 사전 전원 인가 단계 (최초 가동/로그인 시 1회 진행)
+
+
1단계 [장비 확인]: 전원 공급 장치와의 통신 연결 상태를 점검합니다.
+
2단계 [센서 구동 전압 설정]: 센서 보드를 구동하는 데 필요한 전압을 전원 장치에 설정합니다.
+
3단계 [센서 구동 전원 출력]: 설정된 전원을 출력하여 보드를 부팅합니다.
+
(참고: 이 단계는 로그인 성공 시 1회만 실행되며, 이후의 반복 검사 과정에서는 이미 투입된 전원을 재사용하여 4단계부터 즉각 검사가 수행됩니다)
+
+
[2단계] 제품 일련번호 수집 및 센서 통신 대기 단계
+
+
4단계 [판독 장치 대기]: 센서 판독 장치를 초기 대기 상태로 설정합니다.
+
5단계 [보드 통신 연결]: 통신 설정에 명시된 채널을 선택하여 센서 보드와 연결합니다.
+
6단계 [일련번호 읽기]: 제품 메모리로부터 스캔한 고유 일련번호를 로드합니다.
+
7단계 [보드 통신 해제]: 센서 데이터 전송을 중지하고 대기 상태로 되돌립니다.
+
8단계 [보정 기준 전송]: 센서 장치에 보정 기준값을 인계하고 동작을 종료합니다.
+
+
[3단계] 계측기 연동 성능 평가 및 전원 차단 단계
+
+
9단계 [전원 장비 확인]: 주 테스트 전원을 공급할 전원 장치의 통신 상태를 확인합니다.
+
10단계 [테스트 전원 인가]: 테스트 전원 출력을 ON시키고, 전원 인가 후 신호 안정을 위해 잠시 대기합니다.
+
11단계 [측정 장비 확인]: 측정 장치의 통신 상태를 확인합니다.
+
12단계 [전압 측정]: 제품의 현재 작동 전압을 측정합니다.
+
13단계 [저항 측정]: 제품의 전기적 저항값을 직접 실측합니다.
+
14단계 [전류 측정]: 전원 공급 장치의 출력 채널을 통해 제품의 작동 소모 전류를 측정합니다.
+
15단계 [테스트 전원 차단]: 수집 완료 후 테스트 전원 출력을 자동으로 차단하여 제품을 안전하게 보호합니다. (계측 도중 오류가 발생하더라도 강제로 전원이 차단되도록 안전 장치가 적용되어 있습니다.)
+
+
+
7.3 최종 판정 및 마킹 실행 조건
+
계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.
+
+
기준 규격을 벗어난 경우 (FAIL):
+
전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다.
+
이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
+
+
+
모든 항목이 합격인 경우 (PASS):
+
모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
+
합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
+
마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다.
+
+
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
8. 로그 및 데이터 관리
+
+
8.1 시스템 로그 수집 및 관리
+
본 제어 프로그램은 설비 가동 중에 발생하는 계측기와의 통신 상태, 측정값 정보 및 각종 예외 오류 상황을 로그 파일로 기록합니다.
+
8.1.1 로그 파일 보관 방법
+
+
보관 폴더: 프로그램 실행 파일 경로의 하위 Logs 폴더에 저장됩니다.
+
파일명 규칙: 날짜별로 파일이 자동으로 생성되어 보관됩니다.
+
로그 중요도 분류:
+
정보 (INFO): 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록.
+
경고 (WARN): 센서 판독기 인식 지연, 제품 한계 기준치를 벗어난 불합격(FAIL) 발생 기록 등 설비 동작은 유지되나 품질 확인이 필요한 정보.
+
오류 (ERROR): 데이터베이스 네트워크 접속 끊김, 마킹 장치 연결 실패 등 조치가 필요한 장비 오류 정보.
+
+
+
+
+
8.2 장비 교체 및 프로그램 백업 방법
+
장비를 교환하거나 PC 복구 상황에 대비하여, 정상 작동 상태에서의 설정 파일들을 정기적으로 백업해 둘 것을 권장합니다.
+
8.2.1 백업 대상 리스트
+
+
계측기 설정 및 검사 기준값 백업:
+
계측기들의 연결 설정 정보와 합격 판정 기준 범위가 저장됩니다.
+
+
+
데이터베이스 서버 정보 백업:
+
생산 정보 데이터베이스 서버 주소와 로그인 정보가 저장됩니다.
+
+
+
센서 채널 정보 백업:
+
일련번호 판독 통신 포트 번호와 설정 채널 정보가 저장됩니다.
+
+
+
레이저 마킹 도면 파일 백업:
+
제품 표면에 각인할 마킹 도면 파일입니다.
+
+
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
+
9. 오류 대응 및 프로그램 종료
+
+
9.1 현장 장애 유형별 점검 및 예외 조치 요령
+
공정 가동 중 장비 상태나 전산망에 이상이 생길 경우 발생할 수 있는 주요 화면 경고창 및 조치 요령입니다.
+
9.1.1 [오류 1] "제품 ID(IC_SN)를 읽어오지 못했습니다"
+
+
현상: 검사 시작 시 제품 일련번호 읽기 단계에서 경고창이 발생하고 검사가 취소됨.
+
원인 및 해결:
+
접촉 상태 점검: 제품이 안착 지그에 완벽하게 안착되었는지 확인하고, 프레스 실린더 또는 핀 단자의 밀착 상태를 확인하십시오.
+
통신 설정 점검: 프로그램 상단의 통신 설정 창을 열어 현재 설정된 포트 번호 및 물리 채널 번호가 실제 케이블 연결 상태와 일치하는지 확인하십시오.
+
+
+
+
9.1.2 [오류 2] "이전 공정 이력이 조회되지 않는 제품입니다"
+
+
현상: 검사 시작 시 이전 단계 결과가 조회되지 않아 검사가 진행되지 않음.
+
원인 및 해결:
+
해당 제품이 하우징 조립, 센서 보정, EOL 1차 성능 검사 등 이전 가공 공정을 진행하지 않았거나, 이전 설비에서 생산 DB에 데이터를 올바르게 전송하지 않은 상태입니다. 제품 바코드를 재확인하고 이전 설비의 데이터 전송 유무를 검토하십시오.
+
+
+
비상 조치 (강제 검사):
+
이전 이력 등록 상태와 무관하게 제품 성능 계측 및 마킹을 실행하고 결과를 저장해야 하는 특수 테스트 상황인 경우, 화면 최하단의 강제 검사 (스킵) 버튼을 누르면 DB 검증을 건너뛰고 성능 검사를 강제로 실행할 수 있습니다.
+
+
+
+
9.1.3 [오류 3] "이전 공정 단계가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다"
+
+
현상: DB 조회 결과 이전 공정 중 불합격(FAIL 또는 NG) 판정을 받은 내역이 발견되어 검사가 중단됨.
+
원인 및 해결:
+
이전 공정에서 불합격 판정을 받은 제품은 최종 평가 및 레이저 마킹을 가동할 수 없습니다. 불량 제품 격리 기준에 따라 해당 제품을 생산 라인에서 제외 조치하십시오.
+
+
+
비상 조치 (강제 검사):
+
설비 작동 확인이나 품질 분석 등을 위해 반드시 검사를 진행해야 하는 경우, 하단바의 강제 검사 (스킵) 버튼을 클릭하면 경고 상태를 건너뛰고 계측과 마킹을 실행할 수 있습니다.
+
+
+
+
9.1.4 [오류 4] "레이저 각인 작업 전송 실패"
+
+
현상: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함.
+
원인 및 해결:
+
도면 파일 점검: 시험 설정 화면에서 세팅해 놓은 작업 도면 파일이 실제 해당 경로에 존재하는지 점검하십시오.
+
마킹기 전원/연결 점검: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오.
+
+
+
+
9.1.5 [오류 5] "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오."
+
+
현상:
+
프로그램 실행 또는 로그인 완료 후, 메인 화면 상태 알림창에 빨간색 경고 배너로 문구가 표시됩니다.
+
검사 시작 (START) 또는 강제 검사 (스킵) 버튼을 누르면 작동 차단 경고창이 나타나며 설비 검사가 시작되지 않습니다.
+
+
+
원인 및 해결:
+
순번 범위 초과: 현재 로트 번호(Lot No)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다.
+
해결 방법: 새로운 생산 조 또는 로트 번호(Lot No)를 수정하여 프로그램에 재로그인하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다.
+
+
+
+
+
9.2 프로그램 정상 종료 및 전원 자동 차단 규칙
+
본 설비는 고전력 계측 및 전원 제어를 함께 수행하므로 대기 전력 소모 방지와 안전을 위해 아래 절차에 따라 정상 종료 프로세스를 밟아야 합니다.
+
+
로그아웃 (Logout):
+
상단 메뉴의 로그아웃 버튼을 클릭하면 현재 작업자 정보가 해제되고 초기 로그인 창으로 전환됩니다.
+
이와 동시에 연동된 전원 공급 장치로 차단 신호가 자동으로 전송되어 주전원 및 보드 부팅용 상시전원 출력이 OFF 상태로 차단됩니다.
+
+
+
프로그램 완전 종료:
+
화면 우측 상단의 닫기(X) 버튼을 눌러 프로그램을 최종 종료할 때도, 자동으로 상시전원 차단 명령을 장비에 인계한 뒤 레이저 각인 드라이버 리소스를 닫고 메모리를 환원합니다.
+
+
+
+
+
본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.
+
+
\ No newline at end of file
diff --git a/marking_gui/Manual/build_pdf.py b/marking_gui/Manual/build_pdf.py
new file mode 100644
index 0000000..401a126
--- /dev/null
+++ b/marking_gui/Manual/build_pdf.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+import os
+import re
+
+def clean_markdown_content(filename, content, link_mapping):
+ # 1. 목차/이전/다음 등 내비게이션 링크 제거 패턴
+ lines = content.split('\n')
+ cleaned_lines = []
+
+ for line in lines:
+ stripped = line.strip()
+ is_nav = False
+ if "메뉴얼 목차로 돌아가기" in stripped:
+ is_nav = True
+ elif "이전:" in stripped and "다음:" in stripped:
+ is_nav = True
+ elif stripped.startswith("[다음:") and stripped.endswith(")"):
+ is_nav = True
+ elif stripped.startswith("[← 이전:") and stripped.endswith(")"):
+ is_nav = True
+
+ if not is_nav:
+ cleaned_lines.append(line)
+
+ content = '\n'.join(cleaned_lines)
+
+ # 2. Obsidian 위키링크 이미지 문법 -> HTML img 태그로 치환
+ content = re.sub(r'!\[\[(.*?)\]\]', r'', content)
+
+ # 3. 마크다운 파일 참조 링크 -> 로컬 앵커로 치환
+ for md_file, anchor in link_mapping.items():
+ content = content.replace(f"({md_file})", f"({anchor})")
+
+ # 4. 각 파일 시작 전에 페이지 나누기 추가 (표지 파일은 제외)
+ is_cover = "사용자" in filename or "메뉴얼" in filename
+ if not is_cover:
+ content = '\n\n' + content
+
+ # 5. 불필요하게 연속된 빈 라인 및 구분선 정리
+ content = re.sub(r'^\s*---\s*\n', '', content) # 문서 시작 부분의 --- 제거
+ content = re.sub(r'\n\s*---\s*$', '', content) # 문서 끝 부분의 --- 제거
+
+ return content
+
+def main():
+ manual_dir = os.path.dirname(os.path.abspath(__file__))
+ merged_md_path = os.path.join(manual_dir, "Manual_Merged.md")
+ style_path = os.path.join(manual_dir, "pdf_style.css")
+
+ # CSS 파일 로드
+ css_content = ""
+ if os.path.exists(style_path):
+ with open(style_path, "r", encoding="utf-8") as f:
+ css_content = f.read()
+ else:
+ print(f"경고: 스타일시트 파일을 찾을 수 없습니다: {style_path}")
+
+ # 디렉토리 파일들을 UTF-8 유니코드로 스캔
+ all_files = os.listdir(manual_dir)
+
+ # 표지 파일 찾기
+ cover_file = None
+ for f in all_files:
+ if f.endswith(".md") and ("사용자" in f or "메뉴얼" in f) and "Merged" not in f:
+ cover_file = f
+ break
+
+ # "01_" ~ "09_"로 시작하는 검사 매뉴얼 파일 수집
+ numbered_files = []
+ for f in all_files:
+ if f.endswith(".md") and re.match(r'^\d+', f):
+ numbered_files.append(f)
+
+ # 순서 보장을 위해 정렬
+ numbered_files.sort()
+
+ # 최종 병합 리스트 빌드
+ final_file_list = []
+ if cover_file:
+ final_file_list.append(cover_file)
+ final_file_list.extend(numbered_files)
+
+ print("감지 및 병합할 파일 목록:")
+ for f in final_file_list:
+ print(f" - {f}")
+
+ # 링크 치환용 맵 작성 (인코딩 호환성을 위해 파일명 동적 파싱)
+ link_mapping = {}
+ for f in final_file_list:
+ if "사용자" in f or "메뉴얼" in f:
+ link_mapping[f] = "#performance-inspect--laser-marking-system---사용자-메뉴얼"
+ elif f.startswith("01"):
+ link_mapping[f] = "#1-시스템-개요-및-요구사항"
+ elif f.startswith("02"):
+ link_mapping[f] = "#2-초기-설정-가이드"
+ elif f.startswith("03"):
+ link_mapping[f] = "#3-메인-화면-home-view"
+ elif f.startswith("04"):
+ link_mapping[f] = "#4-통신-설정-comm-settings"
+ elif f.startswith("05"):
+ link_mapping[f] = "#5-시험-설정-parameters"
+ elif f.startswith("06"):
+ link_mapping[f] = "#6-측정-데이터-조회-및-데이터베이스-연동"
+ elif f.startswith("07"):
+ link_mapping[f] = "#7-자동-시험-프로세스-및-14단계-시퀀스"
+ elif f.startswith("08"):
+ link_mapping[f] = "#8-로그-및-데이터-관리"
+ elif f.startswith("09"):
+ link_mapping[f] = "#9-오류-대응-및-프로그램-종료"
+
+ merged_markdown_only = ""
+
+ for file_name in final_file_list:
+ file_path = os.path.join(manual_dir, file_name)
+ print(f"병합 처리 중: {file_name}...")
+ with open(file_path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ cleaned = clean_markdown_content(file_name, content, link_mapping)
+ merged_markdown_only += cleaned + "\n\n"
+
+ # md 파일 저장용에는 style 포함
+ merged_content = ""
+ if css_content:
+ merged_content += f"\n\n"
+ merged_content += merged_markdown_only
+
+ # 병합된 마크다운 저장
+ with open(merged_md_path, "w", encoding="utf-8") as f:
+ f.write(merged_content)
+
+ print(f"\n성공: 통합 마크다운 파일이 생성되었습니다 -> {merged_md_path}")
+
+ # HTML 생성 시도
+ try:
+ import markdown
+ merged_html_path = os.path.join(manual_dir, "Manual_for_pdf.html")
+
+ # HTML 뼈대 구성
+ html_output = f"""
+
+
+
+ Performance Inspect & Laser Marking System - 사용자 메뉴얼
+
+
+
+"""
+ html_body = markdown.markdown(merged_markdown_only, extensions=['tables', 'fenced_code'])
+ html_output += html_body
+ html_output += "\n\n"
+
+ with open(merged_html_path, "w", encoding="utf-8") as f:
+ f.write(html_output)
+
+ print(f"성공: 통합 HTML 파일이 생성되었습니다 -> {merged_html_path}")
+ except ImportError:
+ print("\n참고: 파이썬 'markdown' 패키지가 설치되지 않아 HTML 빌드를 건너뜁니다. (pip install markdown)")
+
+if __name__ == "__main__":
+ main()
diff --git a/marking_gui/Manual/pdf_style.css b/marking_gui/Manual/pdf_style.css
new file mode 100644
index 0000000..e909dbc
--- /dev/null
+++ b/marking_gui/Manual/pdf_style.css
@@ -0,0 +1,213 @@
+/*
+ * A4 용지 기준 PDF 출력을 위한 스타일시트 (pdf_style.css)
+ */
+
+@page {
+ size: A4;
+ margin: 20mm 15mm 20mm 15mm;
+ @top-left { content: none !important; }
+ @top-right { content: none !important; }
+ @bottom-left { content: none !important; }
+ @bottom-right { content: none !important; }
+ @bottom-center {
+ content: counter(page) !important;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans KR", sans-serif;
+ font-size: 8.5pt;
+ color: #7f8c8d;
+ }
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans KR", sans-serif;
+ font-size: 10pt;
+ line-height: 1.6;
+ color: #333333;
+ background-color: #ffffff;
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+}
+
+/* 제목 스타일 */
+h1, h2, h3, h4, h5, h6 {
+ color: #111111;
+ font-weight: bold;
+ page-break-after: avoid;
+ page-break-inside: avoid;
+}
+
+h1 {
+ font-size: 18pt;
+ border-bottom: 2px solid #007acc;
+ padding-bottom: 4px;
+ margin-top: 20px;
+ margin-bottom: 12px;
+}
+
+h2 {
+ font-size: 13pt;
+ border-bottom: 1px solid #e1e4e8;
+ padding-bottom: 3px;
+ margin-top: 16px;
+ margin-bottom: 10px;
+}
+
+h3 {
+ font-size: 11pt;
+ margin-top: 12px;
+ margin-bottom: 6px;
+}
+
+h4 {
+ font-size: 10pt;
+ margin-top: 10px;
+ margin-bottom: 6px;
+}
+
+/* 문단 및 리스트 */
+p {
+ margin-top: 0;
+ margin-bottom: 8px;
+ text-align: justify;
+}
+
+ul, ol {
+ margin-top: 0;
+ margin-bottom: 8px;
+ padding-left: 20px;
+}
+
+li {
+ margin-bottom: 3px;
+}
+
+/* 구분선 */
+hr {
+ height: 1px;
+ padding: 0;
+ margin: 15px 0;
+ background-color: #e1e4e8;
+ border: 0;
+}
+
+/* 테이블 스타일 */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin-top: 0;
+ margin-bottom: 10px;
+ page-break-inside: avoid;
+}
+
+table th, table td {
+ padding: 4px 8px;
+ border: 1px solid #dfe2e5;
+ font-size: 8.5pt;
+ word-break: keep-all;
+}
+
+table th {
+ font-weight: 600;
+ background-color: #f6f8fa;
+ text-align: left;
+}
+
+table tr:nth-child(even) {
+ background-color: #f9fbfd;
+}
+
+table tr {
+ page-break-inside: avoid; /* 표의 중간 행이 반으로 쪼개지는 것만 방지 */
+}
+
+/* 인용구 스타일 */
+blockquote {
+ padding: 6px 12px;
+ color: #6a737d;
+ border-left: 0.25em solid #007acc;
+ background-color: #f6f8fa;
+ margin: 0 0 10px 0;
+ page-break-inside: avoid;
+}
+
+blockquote p {
+ margin: 0;
+}
+
+/* 코드 블록 (아스크 아트 등 포함) */
+pre, code {
+ font-family: "Consolas", "Monaco", "Andale Mono", "Ubuntu Mono", monospace;
+ font-size: 8.5pt;
+ background-color: #f6f8fa;
+ border-radius: 3px;
+}
+
+code {
+ padding: 0.2em 0.4em;
+ margin: 0;
+ border: 1px solid #eaecef;
+}
+
+pre {
+ padding: 12px;
+ overflow: auto;
+ line-height: 1.45;
+ border: 1px solid #e1e4e8;
+ word-wrap: normal;
+ white-space: pre; /* 아스키 아트 정렬 유지를 위해 강제 줄바꿈 해제 */
+ page-break-inside: avoid;
+}
+
+pre code {
+ padding: 0;
+ margin: 0;
+ background-color: transparent;
+ border: none;
+ font-size: inherit;
+ white-space: inherit;
+}
+
+/* 이미지 스타일 */
+img {
+ max-width: 100%;
+ max-height: 100mm; /* A4 높이의 약 절반 이하로 제한 */
+ display: block;
+ margin: 15px auto;
+ object-fit: contain;
+ page-break-inside: avoid;
+ border: 1px solid #e1e4e8;
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+}
+
+/* 페이지 나누기 요소 */
+.page-break {
+ page-break-before: always !important;
+}
+
+/* 내비게이션 숨김 (인쇄 시) */
+.nav-link, .nav-links, .back-to-toc, .manual-nav,
+a[href$=".md"] {
+ display: none !important;
+}
+
+/* 링크 스타일 */
+a {
+ color: #007acc;
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+/* 특별 배지 스타일 */
+.badge {
+ display: inline-block;
+ padding: 2px 6px;
+ font-size: 8pt;
+ font-weight: 600;
+ line-height: 1;
+ border-radius: 2px;
+ background-color: #e1e4e8;
+ color: #24292e;
+ margin-right: 4px;
+}
diff --git a/marking_gui/Manual/사용자_메뉴얼.md b/marking_gui/Manual/사용자_메뉴얼.md
new file mode 100644
index 0000000..6d95fab
--- /dev/null
+++ b/marking_gui/Manual/사용자_메뉴얼.md
@@ -0,0 +1,44 @@
+# Performance Inspect & Laser Marking System - 사용자 메뉴얼
+
+> **버전**: 1.0.0 | **최종 수정**: 2026-07-14
+
+---
+
+## 메뉴얼 목차
+
+본 메뉴얼은 각 주제별로 별도 문서로 구성되어 있습니다. 아래 링크를 클릭하여 해당 문서로 이동하세요.
+
+| 번호 | 문서 | 설명 |
+|------|------|------|
+| 1 | [시스템 개요 및 요구사항](01_시스템_개요.md) | 프로그램 소개, 주요 기능, 하드웨어/소프트웨어 요구사항, 장비 구성 |
+| 2 | [초기 설정 가이드](02_초기_설정.md) | 설정 파일의 구성 요소 안내 및 최초 장비 가동 시 점검 순서 |
+| 3 | [메인 화면 (Home View)](03_메인_화면.md) | 상단 정보 표시, 제품 정보 카드, 검사 결과 화면, 진행 상황 확인 및 버튼 제어 가이드 |
+| 4 | [통신 설정 (Comm. Settings)](04_통신_설정.md) | 4251 센서 통신 채널 설정 및 PC 통신 포트 연동 방법 |
+| 5 | [시험 설정 (Parameters)](05_시험_설정.md) | 제품 규격치(전압, 전류, 저항 범위) 및 레이저 마킹 도면 파일 연동 |
+| 6 | [측정 데이터 조회 (Data View)](06_데이터_조회.md) | 생산 데이터베이스를 통한 이전 공정 이력 자동 조회 및 최종 결과 저장 원리 |
+| 7 | [자동 시험 프로세스](07_시험_프로세스.md) | 자동 계측 시퀀스 기동, 마킹 수행 및 최종 합격 판정 흐름 |
+| 8 | [로그 및 데이터 관리](08_로그_관리.md) | 시스템 로그 파일 확인 방법 및 장비 설정 파일 백업 방법 |
+| 9 | [오류 대응 및 프로그램 종료](09_오류_대응.md) | 센서 통신 오류, DB 조회 에러, 마킹 불량 등 현장 장애 대응 및 안전한 프로그램 종료 방법 |
+
+---
+
+## 빠른 시작 가이드 (Quick Start)
+
+처음 시스템을 가동하여 검사를 진행하는 경우 다음 순서에 따라 작업을 수행해 주십시오.
+
+1. **프로그램 설치 (최초 1회)**
+ - 배포받은 단일 설치 프로그램 `MarkingSetup.exe`를 실행하여 클릭 한 번으로 `C:\marking` 경로 및 바탕화면 바로가기를 자동 생성합니다.
+2. **하드웨어 연결 상태 확인**
+ - 계측 장비들과 제어용 PC 간의 네트워크(LAN)선 및 센서 리더용 시리얼 포트 케이블 연결을 확인합니다.
+3. **프로그램 실행 및 로그인**
+ - 바탕화면의 `Marking System` (`marking_gui`) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.)
+3. **통신 상태 확인 및 채널 설정**
+ - 우측 상단의 `통신 설정` 메뉴에서 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.
+4. **검사 규격값 검증**
+ - 우측 상단의 `시험 설정` 메뉴를 열고, 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.
+5. **성능 검사 및 마킹 시작**
+ - 안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 `검사 시작 (START)` 버튼을 눌러 계측 및 마킹을 수행합니다.
+
+---
+
+*본 문서는 Performance Inspect & Laser Marking System v1.0.0 기준으로 작성되었습니다.*
diff --git a/marking_gui/Marking.ico b/marking_gui/Marking.ico
new file mode 100644
index 0000000..b52bc23
Binary files /dev/null and b/marking_gui/Marking.ico differ
diff --git a/marking_gui/Models/MarkingRequest.cs b/marking_gui/Models/MarkingRequest.cs
new file mode 100644
index 0000000..2dc7337
--- /dev/null
+++ b/marking_gui/Models/MarkingRequest.cs
@@ -0,0 +1,70 @@
+using System;
+
+namespace marking_gui.Models
+{
+ public class MarkingRequest
+ {
+ ///
+ /// 제품 일련번호 (SERIAL 태그용)
+ ///
+ public string Serial { get; set; }
+
+ ///
+ /// 센서 IC S/N
+ ///
+ public string IcSerial { get; set; }
+
+ ///
+ /// LOT 번호 (YYMMDD 태그용)
+ ///
+ public string LotNo { get; set; }
+
+ ///
+ /// 제품 고유 번호 (PRODUCT 태그용, 예: 35981-M5000)
+ ///
+ public string ProductNo { get; set; }
+
+ ///
+ /// 전체 QR 코드 문자열 (QRCODETAG 태그용)
+ ///
+ public string QrString { get; set; }
+
+ ///
+ /// 생산 라인 코드 (LCODE 태그용)
+ ///
+ public string LineCode { get; set; }
+
+ ///
+ /// 근무조 정보 (BAN 태그용)
+ ///
+ public string ShiftCode { get; set; }
+
+ ///
+ /// 도면 품번 (DRAWING_NO 태그용)
+ ///
+ public string DrawingNo { get; set; }
+
+ ///
+ /// HKMC 품번 (HKMC_NO 태그용)
+ ///
+ public string HkmcNo { get; set; }
+
+ ///
+ /// 마킹에 사용할 EZD 도안 파일명 (예: AIC1.ezd)
+ ///
+ public string EzdFileName { get; set; }
+
+ public MarkingRequest()
+ {
+ Serial = string.Empty;
+ LotNo = string.Empty;
+ ProductNo = string.Empty;
+ QrString = string.Empty;
+ LineCode = string.Empty;
+ ShiftCode = string.Empty;
+ DrawingNo = string.Empty;
+ HkmcNo = string.Empty;
+ EzdFileName = string.Empty;
+ }
+ }
+}
diff --git a/marking_gui/Models/MarkingResult.cs b/marking_gui/Models/MarkingResult.cs
new file mode 100644
index 0000000..9734d8f
--- /dev/null
+++ b/marking_gui/Models/MarkingResult.cs
@@ -0,0 +1,35 @@
+using System;
+
+namespace marking_gui.Models
+{
+ public class MarkingResult
+ {
+ ///
+ /// 마킹 작업의 성공 여부
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// 에러 코드 (0 이면 성공, 그 외에는 각 에러 상태 코드)
+ ///
+ public int ErrorCode { get; set; }
+
+ ///
+ /// 에러 또는 상세 결과 메시지
+ ///
+ public string ErrorMessage { get; set; }
+
+ ///
+ /// 마킹이 실제 실행 및 완료된 시간
+ ///
+ public DateTime MarkedAt { get; set; }
+
+ public MarkingResult()
+ {
+ Success = false;
+ ErrorCode = -1;
+ ErrorMessage = "Uninitialized";
+ MarkedAt = DateTime.MinValue;
+ }
+ }
+}
diff --git a/marking_gui/Models/MeasurementLimits.cs b/marking_gui/Models/MeasurementLimits.cs
index df839f6..20e5911 100644
--- a/marking_gui/Models/MeasurementLimits.cs
+++ b/marking_gui/Models/MeasurementLimits.cs
@@ -1,4 +1,5 @@
using System;
+using System.IO;
namespace marking_gui.Models
{
@@ -51,5 +52,131 @@ namespace marking_gui.Models
return true;
}
+
+ public static MeasurementLimits LoadFromIni(string iniPath)
+ {
+ var limits = new MeasurementLimits();
+ if (!File.Exists(iniPath)) return limits;
+
+ try
+ {
+ var lines = File.ReadAllLines(iniPath);
+ string currentSection = string.Empty;
+ var dict = new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var line in lines)
+ {
+ string trimmed = line.Trim();
+ if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith(";")) continue;
+ if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
+ {
+ currentSection = trimmed.Substring(1, trimmed.Length - 2).Trim();
+ continue;
+ }
+ if (currentSection.Equals("Limits", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ dict[parts[0].Trim()] = parts[1].Trim();
+ }
+ }
+ }
+
+ if (dict.TryGetValue("MinVoltage", out string minVStr) && double.TryParse(minVStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double minV))
+ limits.MinVoltage = minV;
+ if (dict.TryGetValue("MaxVoltage", out string maxVStr) && double.TryParse(maxVStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double maxV))
+ limits.MaxVoltage = maxV;
+
+ if (dict.TryGetValue("MinCurrent", out string minCStr) && double.TryParse(minCStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double minC))
+ limits.MinCurrent = minC;
+ if (dict.TryGetValue("MaxCurrent", out string maxCStr) && double.TryParse(maxCStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double maxC))
+ limits.MaxCurrent = maxC;
+
+ // 기존 Setting.ini에 uA 단위(8000 등)로 저장되어 있던 데이터를 mA 단위(8.00)로 자동 보정
+ if (limits.MinCurrent > 100) limits.MinCurrent /= 1000.0;
+ if (limits.MaxCurrent > 100) limits.MaxCurrent /= 1000.0;
+
+ if (dict.TryGetValue("MinResistance", out string minRStr) && double.TryParse(minRStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double minR))
+ limits.MinResistance = minR;
+ if (dict.TryGetValue("MaxResistance", out string maxRStr) && double.TryParse(maxRStr, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double maxR))
+ limits.MaxResistance = maxR;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[Limits] ini 로드 실패: " + ex.Message);
+ }
+
+ return limits;
+ }
+
+ public void SaveToIni(string iniPath)
+ {
+ try
+ {
+ var kvMap = new System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["MinVoltage"] = MinVoltage.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
+ ["MaxVoltage"] = MaxVoltage.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
+ ["MinCurrent"] = MinCurrent.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
+ ["MaxCurrent"] = MaxCurrent.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
+ ["MinResistance"] = MinResistance.ToString("F3", System.Globalization.CultureInfo.InvariantCulture),
+ ["MaxResistance"] = MaxResistance.ToString("F3", System.Globalization.CultureInfo.InvariantCulture),
+ };
+
+ System.Collections.Generic.List lines = File.Exists(iniPath) ? new System.Collections.Generic.List(File.ReadAllLines(iniPath)) : new System.Collections.Generic.List();
+ int limitsSectionIndex = -1;
+
+ for (int i = 0; i < lines.Count; i++)
+ {
+ if (lines[i].Trim().Equals("[Limits]", StringComparison.OrdinalIgnoreCase))
+ {
+ limitsSectionIndex = i;
+ break;
+ }
+ }
+
+ if (limitsSectionIndex >= 0)
+ {
+ int insertPos = limitsSectionIndex + 1;
+ while (insertPos < lines.Count && !lines[insertPos].Trim().StartsWith("["))
+ {
+ string line = lines[insertPos].Trim();
+ int eq = line.IndexOf('=');
+ if (eq > 0)
+ {
+ string key = line.Substring(0, eq).Trim();
+ if (kvMap.ContainsKey(key))
+ {
+ lines[insertPos] = $"{key}={kvMap[key]}";
+ kvMap.Remove(key);
+ }
+ }
+ insertPos++;
+ }
+
+ foreach (var kv in kvMap)
+ {
+ lines.Insert(insertPos, $"{kv.Key}={kv.Value}");
+ insertPos++;
+ }
+ }
+ else
+ {
+ lines.Add("");
+ lines.Add("[Limits]");
+ foreach (var kv in kvMap)
+ {
+ lines.Add($"{kv.Key}={kv.Value}");
+ }
+ }
+
+ File.WriteAllLines(iniPath, lines);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[Limits] ini 저장 실패: " + ex.Message);
+ }
+ }
}
}
diff --git a/marking_gui/Models/Product.cs b/marking_gui/Models/Product.cs
index 3edba69..8c2e7a8 100644
--- a/marking_gui/Models/Product.cs
+++ b/marking_gui/Models/Product.cs
@@ -8,12 +8,16 @@ namespace marking_gui.Models
public string HousingResult { get; set; }
public string CalResult { get; set; }
public string EolResult { get; set; }
+ public string PcbBarcode { get; set; }
public bool IsPreviousStepsPassed
{
get
{
- return HousingResult == "PASS" && CalResult == "PASS" && EolResult == "PASS";
+ bool housingOk = HousingResult == "PASS" || HousingResult == "OK";
+ 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
new file mode 100644
index 0000000..e0e6018
--- /dev/null
+++ b/marking_gui/PortSettingsWindow.xaml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/marking_gui/PortSettingsWindow.xaml.cs b/marking_gui/PortSettingsWindow.xaml.cs
new file mode 100644
index 0000000..f6108aa
--- /dev/null
+++ b/marking_gui/PortSettingsWindow.xaml.cs
@@ -0,0 +1,156 @@
+using System;
+using System.IO;
+using System.IO.Ports;
+using System.Windows;
+
+namespace marking_gui
+{
+ ///
+ /// PortSettingsWindow.xaml에 대한 상호 작용 논리
+ ///
+ public partial class PortSettingsWindow : Window
+ {
+ private string _iniPath;
+
+ public PortSettingsWindow()
+ {
+ InitializeComponent();
+ _iniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sensor.ini");
+
+ Loaded += PortSettingsWindow_Loaded;
+ }
+
+ private void PortSettingsWindow_Loaded(object sender, RoutedEventArgs e)
+ {
+ // 1. 시스템에 유효한 COM 포트 목록 조회
+ string[] ports = SerialPort.GetPortNames();
+ 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");
+ cbBaudRate.Items.Add("38400");
+ 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))
+ {
+ try
+ {
+ var lines = File.ReadAllLines(_iniPath);
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+ if (trimmed.StartsWith("Port", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2) currentPort = parts[1].Trim();
+ }
+ else if (trimmed.StartsWith("BaudRate", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2) currentBaud = parts[1].Trim();
+ }
+ else if (trimmed.StartsWith("Channel", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2) currentChannel = parts[1].Trim();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ 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 (cbChannel.Items.Contains(currentChannel))
+ {
+ cbChannel.SelectedItem = currentChannel;
+ }
+ else
+ {
+ cbChannel.SelectedItem = "1";
+ }
+ }
+
+ private void btnSave_Click(object sender, RoutedEventArgs e)
+ {
+ if (cbPort.SelectedItem == null || cbBaudRate.SelectedItem == null || cbChannel.SelectedItem == null)
+ {
+ MessageBox.Show("포트, 통신 속도 및 채널을 선택해 주십시오.", "알림", MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
+ string port = cbPort.SelectedItem.ToString();
+ string baud = cbBaudRate.SelectedItem.ToString();
+ string channel = cbChannel.SelectedItem.ToString();
+
+ try
+ {
+ // Sensor.ini 에 저장
+ using (var sw = new StreamWriter(_iniPath, 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);
+ }
+
+ DialogResult = true;
+ Close();
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show("설정을 저장하는 중 오류가 발생했습니다: " + ex.Message, "저장 실패", MessageBoxButton.OK, MessageBoxImage.Error);
+ }
+ }
+
+ private void btnCancel_Click(object sender, RoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+ }
+}
diff --git a/marking_gui/Properties/PublishProfiles/FolderProfile.pubxml b/marking_gui/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 0000000..5c691d9
--- /dev/null
+++ b/marking_gui/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,15 @@
+
+
+
+
+ Release
+ Any CPU
+ C:\Users\COMPUTER1\Downloads
+ FileSystem
+ <_TargetId>Folder
+ net9.0-windows
+ win-x86
+ true
+ false
+
+
\ No newline at end of file
diff --git a/marking_gui/Services/Board4251Service.cs b/marking_gui/Services/Board4251Service.cs
new file mode 100644
index 0000000..68196ba
--- /dev/null
+++ b/marking_gui/Services/Board4251Service.cs
@@ -0,0 +1,389 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace marking_gui.Services
+{
+ ///
+ /// 제품 ID 센서에서 읽은 제품 ID 정보.
+ ///
+ public class SensorIdData
+ {
+ public int Year { get; set; }
+ public int Month { get; set; }
+ public int Day { get; set; }
+ public string Serial { get; set; } = "";
+ public string McLine { get; set; } = "";
+ public string LineNo { get; set; } = "";
+ public string Item { get; set; } = "";
+ public string ID { get; set; } = "";
+ }
+
+ ///
+ /// 제품 ID 센서 서비스의 추상 인터페이스.
+ ///
+ public interface IIdSensorService : IDisposable
+ {
+ event EventHandler ProgressMessage;
+ event EventHandler ErrorMessage;
+ event EventHandler ConnectionChanged;
+
+ bool Connect();
+ void Disconnect();
+ Task ReadSensorAsync();
+ }
+
+ ///
+ /// 신규 4251 보드와의 통신을 관리하는 서비스.
+ ///
+ public class Board4251Service : IDisposable
+ {
+ private readonly ICommunication _communication;
+ private readonly StringBuilder _receiveBuffer = new StringBuilder();
+ private readonly object _bufferLock = new object();
+ private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
+ private TaskCompletionSource _currentTcs;
+ public int TimeoutMs { get; set; } = 5000;
+ private bool _shouldBeConnected = false;
+ private System.Timers.Timer _reconnectTimer;
+
+ public event EventHandler ErrorOccurred;
+ public event EventHandler ConnectionChanged;
+
+ public string LastResponse { get; private set; } = "";
+
+ public string GetLastBuffer()
+ {
+ string currentBuf;
+ lock (_bufferLock)
+ {
+ currentBuf = _receiveBuffer.ToString().Trim();
+ }
+ return string.IsNullOrEmpty(currentBuf) ? LastResponse : currentBuf;
+ }
+
+ public Board4251Service(ICommunication communication)
+ {
+ _communication = communication;
+ _communication.DataReceived += OnDataReceived;
+ _communication.ConnectionStatusChanged += (s, isConnected) => {
+ ConnectionChanged?.Invoke(this, isConnected);
+ if (!isConnected && _shouldBeConnected) StartReconnectTimer();
+ };
+
+ _reconnectTimer = new System.Timers.Timer(1000);
+ _reconnectTimer.AutoReset = false;
+ _reconnectTimer.Elapsed += (s, e) => {
+ if (_shouldBeConnected && !_communication.IsOpen)
+ {
+ if (!_communication.Open())
+ {
+ if (_shouldBeConnected) _reconnectTimer.Start();
+ }
+ }
+ else if (_shouldBeConnected)
+ {
+ _reconnectTimer.Start();
+ }
+ };
+ }
+
+ private void StartReconnectTimer()
+ {
+ if (_reconnectTimer != null && !_reconnectTimer.Enabled) _reconnectTimer.Start();
+ }
+
+ public bool Connect()
+ {
+ _shouldBeConnected = true;
+ bool opened = _communication.Open();
+ if (!opened) StartReconnectTimer();
+ return opened;
+ }
+
+ public void Disconnect()
+ {
+ _shouldBeConnected = false;
+ _reconnectTimer?.Stop();
+ _communication.Close();
+ }
+
+ public async Task CheckStatusAsync(int channel = 1)
+ {
+ string response = await SendCommandAsync($"x00c_00{channel}101:owt28006727ea97c7801\r\n");
+ if (response == null) return false;
+ if (response.IndexOf("Success", StringComparison.OrdinalIgnoreCase) >= 0) return true;
+ if (response.IndexOf("Fail", StringComparison.OrdinalIgnoreCase) >= 0) return false;
+ return false;
+ }
+
+ public async Task ReadIdAsync(int channel = 1)
+ {
+ string response = await SendCommandAsync($"x00c_00{channel}101:ow2800326003e\r\n");
+ if (response == null) return null;
+
+ // 16자리 16진수 또는 ID 형태 파싱 추출
+ // 보드 응답 예: "x00c_001101:ow2800326003e:Success:1234567890ABCDEF" 형태
+ // 혹은 단순히 "1234567890ABCDEF" 형태의 데이터가 수신됨
+ try
+ {
+ string[] parts = response.Split(':');
+ if (parts.Length >= 4)
+ {
+ return parts[parts.Length - 1].Trim();
+ }
+ return response.Trim();
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[Board4251] ReadIdAsync 파싱 예외: " + ex.Message);
+ return response.Trim();
+ }
+ }
+
+ private string _activeCommand = null;
+
+ public async Task SendCommandAsync(string command)
+ {
+ await _lock.WaitAsync();
+ try
+ {
+ lock (_bufferLock)
+ {
+ _receiveBuffer.Clear();
+ }
+ var tcs = new TaskCompletionSource();
+ _currentTcs = tcs;
+ _activeCommand = command;
+
+ _communication.ClearBuffer();
+ if (!_communication.Write(command))
+ {
+ return null;
+ }
+
+ using (var cts = new CancellationTokenSource(TimeoutMs))
+ {
+ // 타임아웃 발생 시 TCS 취소 상태 전달
+ cts.Token.Register(() => tcs.TrySetCanceled(), useSynchronizationContext: false);
+
+ try
+ {
+ string response = await tcs.Task;
+ if (response != null)
+ {
+ response = response.Replace("", "").Trim();
+ }
+ return response;
+ }
+ catch (TaskCanceledException)
+ {
+ LoggerService.Warn($"[Board4251] 명령 전송 타임아웃 ({TimeoutMs}ms): {command.Trim()}");
+ lock (_bufferLock)
+ {
+ LastResponse = _receiveBuffer.ToString().Trim();
+ }
+ return null;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ ErrorOccurred?.Invoke(this, "명령 전송 예외: " + ex.Message);
+ return null;
+ }
+ finally
+ {
+ _currentTcs = null;
+ _activeCommand = null;
+ _lock.Release();
+ }
+ }
+
+ private void OnDataReceived(object sender, string data)
+ {
+ if (string.IsNullOrEmpty(data)) return;
+
+ string currentData;
+ lock (_bufferLock)
+ {
+ _receiveBuffer.Append(data);
+ currentData = _receiveBuffer.ToString();
+ }
+
+ var targetTcs = _currentTcs;
+ if (targetTcs == null || targetTcs.Task.IsCompleted) return;
+
+ // 기반 완료 확인 (시스템상 종단 확인 메커니즘 우선 적용)
+ if (_activeCommand != null && _activeCommand.Contains("x00c_"))
+ {
+ if (currentData.Contains(""))
+ {
+ targetTcs.TrySetResult(currentData.Trim());
+ }
+ return;
+ }
+
+ // 가 명시되지 않는 기타 일반 패킷은 개행(\n) 또는 Success/Fail 기반으로 완료 판정
+ if (currentData.Contains("") || currentData.Contains("\n") || currentData.Contains("Success") || currentData.Contains("Fail"))
+ {
+ targetTcs.TrySetResult(currentData.Trim());
+ }
+ }
+
+ public void Dispose()
+ {
+ Disconnect();
+ _reconnectTimer?.Dispose();
+ _lock.Dispose();
+ }
+ }
+
+ ///
+ /// 신규 4251 보드를 사용하여 제품 ID를 읽는 센서 서비스.
+ ///
+ public class Board4251SensorService : IIdSensorService
+ {
+ private readonly Board4251Service _service;
+ private readonly int _sensorIndex;
+ private readonly EventHandler _connectionChangedHandler;
+
+ public event EventHandler ProgressMessage;
+ public event EventHandler ErrorMessage;
+ public event EventHandler ConnectionChanged;
+
+ public Board4251SensorService(Board4251Service service, int sensorIndex)
+ {
+ _service = service;
+ _sensorIndex = sensorIndex;
+ _connectionChangedHandler = (s, isConnected) => ConnectionChanged?.Invoke(this, isConnected);
+ _service.ConnectionChanged += _connectionChangedHandler;
+ }
+
+ public bool Connect() => _service.Connect();
+ public void Disconnect() => _service.Disconnect();
+
+ public async Task ReadSensorAsync()
+ {
+ try
+ {
+ int channel = _sensorIndex + 1;
+ ProgressMessage?.Invoke(this, "4251 보드 초기화 중...");
+ _ = _service.SendCommandAsync("x00o\r\n");
+ await Task.Delay(350);
+
+ ProgressMessage?.Invoke(this, "4251 보드 데이터 읽는 중...");
+ int extendedTimeout = 15000;
+ string statusCmd = $"x00c_00{channel}101:owt28006727ea97c7801";
+ bool statusResult = false;
+
+ using (var cts = new CancellationTokenSource(extendedTimeout))
+ {
+ var statusTask = _service.CheckStatusAsync(channel);
+ var delayTask = Task.Delay(extendedTimeout, cts.Token);
+ var completedTask = await Task.WhenAny(statusTask, delayTask);
+ if (completedTask == delayTask)
+ {
+ string buf = _service.GetLastBuffer();
+ ErrorMessage?.Invoke(this, $"CH{channel} 상태 타임아웃\r\n송신: {statusCmd}\r\n수신: {buf}");
+ return null;
+ }
+ cts.Cancel();
+ statusResult = await statusTask;
+ }
+
+ if (!statusResult)
+ {
+ string buf = _service.GetLastBuffer();
+ ErrorMessage?.Invoke(this, $"CH{channel} 상태 실패\r\n송신: {statusCmd}\r\n수신: {buf}");
+ return null;
+ }
+
+ // ID 읽기 (최대 3회 시도)
+ string idCmd = $"x00c_00{channel}101:ow2800326003e";
+ string rawId = null;
+ int maxAttempts = 3;
+
+ for (int attempt = 1; attempt <= maxAttempts; attempt++)
+ {
+ using (var cts = new CancellationTokenSource(extendedTimeout))
+ {
+ var idTask = _service.ReadIdAsync(channel);
+ var delayTask = Task.Delay(extendedTimeout, cts.Token);
+ var completedTask = await Task.WhenAny(idTask, delayTask);
+ if (completedTask == delayTask)
+ {
+ if (attempt == maxAttempts)
+ {
+ string buf = _service.GetLastBuffer();
+ ErrorMessage?.Invoke(this, $"CH{channel} ID 타임아웃\r\n송신: {idCmd}\r\n수신: {buf}");
+ return null;
+ }
+ continue;
+ }
+ cts.Cancel();
+ rawId = await idTask;
+ }
+
+ if (string.IsNullOrEmpty(rawId))
+ {
+ if (attempt == maxAttempts)
+ {
+ string buf = _service.GetLastBuffer();
+ ErrorMessage?.Invoke(this, $"CH{channel} ID 응답 없음\r\n송신: {idCmd}\r\n수신: {buf}");
+ return null;
+ }
+ continue;
+ }
+
+ if (rawId.EndsWith("F", StringComparison.OrdinalIgnoreCase))
+ {
+ if (attempt < maxAttempts)
+ {
+ ProgressMessage?.Invoke(this, $"ID 읽기 재시도 중... ({attempt}/{maxAttempts})");
+ await Task.Delay(350);
+ continue;
+ }
+ }
+ break;
+ }
+
+ // Step 7: 보드 OFF/후처리 (x00o)
+ ProgressMessage?.Invoke(this, "4251 보드 OFF/후처리 중...");
+ await _service.SendCommandAsync("x00o\r\n");
+ await Task.Delay(350);
+
+ // Step 8: 보드 CAL 기본값 (x00c_00{channel}001:owt28006727ea97c7801)
+ ProgressMessage?.Invoke(this, "4251 보드 CAL 기본값 설정 중...");
+ string calCmd = $"x00c_00{channel}001:owt28006727ea97c7801\r\n";
+ await _service.SendCommandAsync(calCmd);
+ await Task.Delay(350);
+
+ var data = new SensorIdData
+ {
+ ID = rawId,
+ Serial = "",
+ Item = "N/A"
+ };
+
+ ProgressMessage?.Invoke(this, "ID 읽기 성공: " + rawId);
+ return data;
+ }
+ catch (Exception ex)
+ {
+ ErrorMessage?.Invoke(this, $"4251 읽기 예외: {ex.Message}");
+ return null;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_service != null && _connectionChangedHandler != null)
+ {
+ _service.ConnectionChanged -= _connectionChangedHandler;
+ }
+ }
+ }
+}
diff --git a/marking_gui/Services/CommunicationBase.cs b/marking_gui/Services/CommunicationBase.cs
new file mode 100644
index 0000000..d8081e2
--- /dev/null
+++ b/marking_gui/Services/CommunicationBase.cs
@@ -0,0 +1,142 @@
+using System;
+using System.IO.Ports;
+
+namespace marking_gui.Services
+{
+ ///
+ /// 하드웨어 통신을 위한 추상화 인터페이스.
+ /// 시리얼(RS232), 이더넷(TCP/IP) 등 통신 방식에 상관없이
+ /// 상위 서비스에서 동일한 방식으로 하드웨어에 접근할 수 있도록 정의함.
+ ///
+ public interface ICommunication
+ {
+ /// 통신 채널 식별 이름 (예: COM1, TCP_192.168.0.1)
+ string Name { get; }
+
+ /// 현재 통신 채널이 열려 있는지 여부
+ bool IsOpen { get; }
+
+ /// 통신 채널을 연결함
+ bool Open();
+
+ /// 통신 채널 연결을 해제함
+ void Close();
+
+ /// 데이터를 하드웨어로 전송함
+ bool Write(string data);
+
+ /// 수신 버퍼를 비워 잔류 데이터를 제거함
+ void ClearBuffer();
+
+ /// 하드웨어로부터 데이터를 수신했을 때 발생하는 이벤트
+ event System.EventHandler DataReceived;
+
+ /// 연결 상태가 변경되었을 때 발생하는 이벤트 (connected, disconnected)
+ event System.EventHandler ConnectionStatusChanged;
+ }
+
+ ///
+ /// RS232 시리얼 통신을 담당하는 구현체.
+ /// 연결 안정성 강화를 위한 예외 처리 및 상태 감지 로직 포함.
+ ///
+ public class SerialProvider : ICommunication, IDisposable
+ {
+ private SerialPort _serialPort;
+ private bool _disposed = false;
+
+ public string Name { get; private set; }
+ public bool IsOpen => _serialPort != null && !_disposed && _serialPort.IsOpen;
+
+ public event EventHandler DataReceived;
+ public event EventHandler ConnectionStatusChanged;
+
+ public SerialProvider(string portName, int baudRate)
+ {
+ Name = portName;
+ _serialPort = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
+ _serialPort.Handshake = Handshake.None;
+ _serialPort.DtrEnable = true; // 터미널 프로그램(포트몬)과 동일하게 제어 신호 활성화
+ _serialPort.RtsEnable = true;
+ _serialPort.ReadTimeout = 500;
+ _serialPort.WriteTimeout = 500;
+
+ _serialPort.DataReceived += (s, e) => {
+ try {
+ string data = _serialPort.ReadExisting();
+ if (!string.IsNullOrEmpty(data))
+ DataReceived?.Invoke(this, data);
+ } catch (Exception ex) {
+ Console.WriteLine($"[Serial Error] Read error on {Name}: {ex.Message}");
+ }
+ };
+
+ // 기기가 끊겼을 때 감지 (일부 환경에서는 PinChanged 등으로 감지 가능)
+ _serialPort.ErrorReceived += (s, e) => {
+ ConnectionStatusChanged?.Invoke(this, false);
+ };
+ }
+
+ public bool Open()
+ {
+ if (_disposed) return false;
+ try {
+ if (IsOpen) return true;
+ _serialPort.Open();
+ ConnectionStatusChanged?.Invoke(this, true);
+ return true;
+ } catch (Exception ex) {
+ Console.WriteLine($"[Serial Error] Failed to open {Name}: {ex.Message}");
+ ConnectionStatusChanged?.Invoke(this, false);
+ return false;
+ }
+ }
+
+ public void Close()
+ {
+ try {
+ if (IsOpen) {
+ _serialPort.Close();
+ ConnectionStatusChanged?.Invoke(this, false);
+ }
+ } catch (Exception ex) {
+ Console.WriteLine($"[Serial Error] Error closing {Name}: {ex.Message}");
+ }
+ }
+
+ public bool Write(string data)
+ {
+ if (!IsOpen) return false;
+ try {
+ _serialPort.Write(data);
+ return true;
+ } catch (Exception ex) {
+ Console.WriteLine($"[Serial Error] Write error on {Name}: {ex.Message}");
+ return false;
+ }
+ }
+
+ public void ClearBuffer()
+ {
+ if (IsOpen) {
+ try {
+ _serialPort.DiscardInBuffer();
+ _serialPort.DiscardOutBuffer();
+ _serialPort.ReadExisting();
+ } catch (Exception ex) {
+ System.Diagnostics.Debug.WriteLine($"[CommunicationBase ClearBuffer Error] {ex.Message}");
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ Close();
+ _serialPort?.Dispose();
+ _serialPort = null;
+ }
+ }
+ }
+}
diff --git a/marking_gui/Services/DatabaseService.cs b/marking_gui/Services/DatabaseService.cs
new file mode 100644
index 0000000..a1e329a
--- /dev/null
+++ b/marking_gui/Services/DatabaseService.cs
@@ -0,0 +1,406 @@
+using System;
+using System.Configuration;
+using marking_gui.Models;
+
+namespace marking_gui.Services
+{
+ public class DatabaseService
+ {
+ private readonly string _connectionString;
+ private readonly bool _offlineMode = false;
+
+ public bool OfflineMode => _offlineMode;
+
+ public DatabaseService()
+ {
+ try
+ {
+ string dbServer = string.Empty;
+ string dbName = string.Empty;
+ string dbUser = string.Empty;
+ string dbPassword = string.Empty;
+ string rawConnStr = string.Empty;
+
+ // AppDomain.CurrentDomain.BaseDirectory 에서 Database.ini 로드 시도
+ string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Database.ini");
+ if (System.IO.File.Exists(iniPath))
+ {
+ var lines = System.IO.File.ReadAllLines(iniPath);
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+ if (trimmed.StartsWith(";") || trimmed.StartsWith("#"))
+ {
+ continue;
+ }
+
+ if (trimmed.StartsWith("ConnectionString", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ rawConnStr = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("Db_Server", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ dbServer = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("Db_Name", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ dbName = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("Db_User", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ dbUser = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("Db_Password", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ dbPassword = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("OfflineMode", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ bool.TryParse(parts[1].Trim(), out _offlineMode);
+ }
+ }
+ }
+ }
+
+ // 개별 설정이 있는 경우 조합해서 사용
+ if (!string.IsNullOrWhiteSpace(dbServer) &&
+ !string.IsNullOrWhiteSpace(dbName) &&
+ !string.IsNullOrWhiteSpace(dbUser) &&
+ !string.IsNullOrWhiteSpace(dbPassword))
+ {
+ _connectionString = $"Server={dbServer};Database={dbName};User Id={dbUser};Password={dbPassword};";
+ }
+ else
+ {
+ _connectionString = rawConnStr;
+ }
+
+ // Database.ini 에 없을 경우 App.config 백업 사용
+ if (string.IsNullOrEmpty(_connectionString))
+ {
+ var connSettings = ConfigurationManager.ConnectionStrings["MssqlConnection"];
+ if (connSettings != null)
+ {
+ _connectionString = connSettings.ConnectionString;
+ }
+ else
+ {
+ _connectionString = "Server=localhost;Database=MarkingDb;Trusted_Connection=True;";
+ }
+ }
+
+ // SSL 인증서 체인 신뢰성 오류(TrustServerCertificate=True 누락) 방지 조치 추가
+ if (!string.IsNullOrWhiteSpace(_connectionString) && _connectionString.IndexOf("TrustServerCertificate", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ if (!_connectionString.EndsWith(";")) _connectionString += ";";
+ _connectionString += "TrustServerCertificate=True;";
+ }
+
+ // DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 3초로 강제 조정
+ if (!string.IsNullOrWhiteSpace(_connectionString) &&
+ _connectionString.IndexOf("Connect Timeout", StringComparison.OrdinalIgnoreCase) < 0 &&
+ _connectionString.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ if (!_connectionString.EndsWith(";")) _connectionString += ";";
+ _connectionString += "Connect Timeout=3;";
+ }
+ }
+ catch (Exception ex)
+ {
+ _connectionString = string.Empty;
+ System.Diagnostics.Debug.WriteLine("설정 로드 중 예외 발생: " + ex.Message);
+ }
+ }
+
+ public bool CheckConnection()
+ {
+ if (_offlineMode)
+ return true;
+
+ if (string.IsNullOrWhiteSpace(_connectionString))
+ return false;
+
+ try
+ {
+ using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
+ {
+ conn.Open();
+ return true;
+ }
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public Product GetProductBySerial(string serial)
+ {
+ if (string.IsNullOrEmpty(serial))
+ return null;
+
+ string searchSerial = serial.Trim().ToUpper();
+
+ // 오프라인 모드인 경우 데이터베이스가 없으므로 항상 이전 공정 통과(PASS)로 가정하여 가상 데이터 반환
+ if (_offlineMode)
+ {
+ System.Diagnostics.Debug.WriteLine("[OfflineMode] 제품 조회 우회 (오프라인 모드 활성화). S/N: " + searchSerial);
+ return new Product(searchSerial, "PASS", "PASS", "PASS");
+ }
+
+ try
+ {
+ // 이전 3개 공정 테이블(Housing_Assembly, Cal_EOL_Result, EOL)에서 IC_SN 기준으로 Result 각각 조회
+ // Housing_Assembly 테이블에서 PCB_Barcode 가져옴
+ string query = @"
+ SELECT
+ (SELECT TOP 1 [Result] FROM [dbo].[Housing_Assembly] WHERE UPPER([IC_SN]) = @Serial) AS HousingResult,
+ (SELECT TOP 1 [Final_Result] FROM [dbo].[Cal_EOL_Result] WHERE UPPER([IC_SN]) = @Serial) AS CalResult,
+ (SELECT TOP 1 [Result] FROM [dbo].[EOL] WHERE UPPER([IC_SN]) = @Serial) AS EolResult,
+ (SELECT TOP 1 [PCB_Barcode] FROM [dbo].[Housing_Assembly] WHERE UPPER([IC_SN]) = @Serial) AS PcbBarcode";
+
+ using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
+ {
+ using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
+ {
+ cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
+ cmd.Parameters.AddWithValue("@Serial", searchSerial);
+ conn.Open();
+
+ using (var reader = cmd.ExecuteReader())
+ {
+ if (reader.Read())
+ {
+ // 데이터베이스 값이 NULL 이거나 빈 값일 경우 "대기"로 처리
+ string housingRes = reader["HousingResult"] != DBNull.Value ? reader["HousingResult"].ToString().Trim() : "대기";
+ string calRes = reader["CalResult"] != DBNull.Value ? reader["CalResult"].ToString().Trim() : "대기";
+ string eolRes = reader["EolResult"] != DBNull.Value ? reader["EolResult"].ToString().Trim() : "대기";
+ string pcbBarcode = reader["PcbBarcode"] != DBNull.Value ? reader["PcbBarcode"].ToString().Trim() : string.Empty;
+
+ // 세 공정 중 하나라도 데이터 흔적이 있는 경우 Product 객체 생성 반환
+ if (housingRes != "대기" || calRes != "대기" || eolRes != "대기")
+ {
+ return new Product(searchSerial, housingRes, calRes, eolRes)
+ {
+ PcbBarcode = pcbBarcode
+ };
+ }
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[DB GetProduct] 이전 공정 결과 조회 중 에러: " + ex.Message);
+ }
+
+ return null;
+ }
+
+ ///
+ /// 최종 성능 검출 값을 데이터베이스에 저장합니다.
+ /// 전압과 전류 수치는 3,2 포맷(DECIMAL(3, 2) 등)에 대응하기 위해 소수점 이하 2자리로 반올림하여 처리합니다.
+ ///
+ public bool SaveProductResult(
+ string serial,
+ string maker,
+ string model,
+ string variant1,
+ string variant2,
+ string operatorId,
+ string line,
+ string lotNo,
+ string jigNo,
+ double voltage,
+ double current,
+ double resistance,
+ string result,
+ DateTime startTime,
+ DateTime endTime,
+ string markedSerial,
+ string pcbBarcode = null,
+ string drawingNo = "",
+ string hkmcNo = "")
+ {
+ // 전압과 전류, 저항을 소수점 이하 2자리(3,2 포맷 규격)로 반올림 처리
+ decimal roundedVoltage = Math.Round((decimal)voltage, 2);
+ decimal roundedCurrent = Math.Round((decimal)current, 2);
+ decimal roundedResistance = Math.Round((decimal)resistance, 3);
+
+ // IC_SN 및 PCB_Barcode 컬럼 규격(NVARCHAR(30), NOT NULL) 방어 처리
+ if (string.IsNullOrWhiteSpace(serial))
+ {
+ serial = "UNKNOWN_" + DateTime.Now.ToString("HHmmss");
+ }
+ if (serial.Length > 30)
+ {
+ serial = serial.Substring(0, 30);
+ }
+ // 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);
+ string safeModel = string.IsNullOrEmpty(model) ? "" : (model.Length > 10 ? model.Substring(0, 10) : model);
+ string safeVariant1 = string.IsNullOrEmpty(variant1) ? "" : (variant1.Length > 10 ? variant1.Substring(0, 10) : variant1);
+ string safeVariant2 = string.IsNullOrEmpty(variant2) ? "" : (variant2.Length > 10 ? variant2.Substring(0, 10) : variant2);
+ string safeOperator = string.IsNullOrEmpty(operatorId) ? "" : (operatorId.Length > 10 ? operatorId.Substring(0, 10) : operatorId);
+ string safeLine = string.IsNullOrEmpty(line) ? "" : (line.Length > 10 ? line.Substring(0, 10) : line);
+ string safeLotNo = string.IsNullOrEmpty(lotNo) ? "" : (lotNo.Length > 10 ? lotNo.Substring(0, 10) : lotNo);
+ string safeJigNo = string.IsNullOrEmpty(jigNo) ? "" : (jigNo.Length > 10 ? jigNo.Substring(0, 10) : jigNo);
+
+ DateTime prodDate = endTime;
+ // QR Code 조합 포맷 생성 ([IC S/N];[도면품번];[HKMC품번];[제품 S/N];) (Marking_QR: NVARCHAR(60))
+ string markingQr = string.Format("{0};{1};{2};{3};", serial, drawingNo ?? "", hkmcNo ?? "", markedSerial);
+ if (markingQr.Length > 60)
+ {
+ markingQr = markingQr.Substring(0, 60);
+ }
+
+ // 오프라인 모드인 경우 실제 DB 저장을 생략하고 정상 통과 처리
+ if (_offlineMode)
+ {
+ System.Diagnostics.Debug.WriteLine(string.Format(
+ "[OfflineMode] DB 저장 생략 (오프라인 모드 활성화) S/N: {0} | 각인번호: {1} | QR: {2} | Maker: {3} | Model: {4} | Line: {5} | LotNo: {6} | Vol_1: {7:F2}V | Cur_1: {8:F2}mA | Res_1: {9:F3}kΩ | 결과: {10} | 시작:{11} | 종료:{12}",
+ serial, markedSerial, markingQr, safeMaker, safeModel, safeLine, safeLotNo, roundedVoltage, roundedCurrent, roundedResistance, result,
+ startTime.ToString("HH:mm:ss"), endTime.ToString("HH:mm:ss")));
+ return true;
+ }
+
+ // 실제 MSSQL DB 적재 실행 (동일 IC_SN 재검사 시 데이터가 덮어씌워지도록 MERGE 문 도입)
+ try
+ {
+ string query = @"MERGE INTO [dbo].[Marking] AS target
+ USING (SELECT @Serial AS IC_SN) AS source
+ ON (target.[IC_SN] = source.IC_SN)
+ WHEN MATCHED THEN
+ UPDATE SET
+ [Maker] = @Maker,
+ [Model] = @Model,
+ [Variant_1] = @Var1,
+ [Variant_2] = @Var2,
+ [Operator] = @Operator,
+ [Production_Date] = @ProdDate,
+ [Line] = @Line,
+ [Lot_No] = @LotNo,
+ [JIG_No] = @JigNo,
+ [PCB_Barcode] = @PcbBarcode,
+ [Process_Label] = @ProcessLabel,
+ [Process_Label_Scan] = @ProcessLabelScan,
+ [PT_Vol_1] = @Vol1,
+ [PT_Current_1] = @Cur1,
+ [Marking_QR] = @Qr,
+ [Result] = @Result,
+ [Error_Code] = @ErrorCode
+ 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);";
+
+ 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("@Maker", safeMaker);
+ cmd.Parameters.AddWithValue("@Model", safeModel);
+ cmd.Parameters.AddWithValue("@Var1", safeVariant1);
+ cmd.Parameters.AddWithValue("@Var2", safeVariant2);
+ cmd.Parameters.AddWithValue("@Operator", safeOperator);
+ cmd.Parameters.AddWithValue("@ProdDate", prodDate);
+ cmd.Parameters.AddWithValue("@Line", safeLine);
+ cmd.Parameters.AddWithValue("@LotNo", safeLotNo);
+ cmd.Parameters.AddWithValue("@JigNo", safeJigNo);
+ cmd.Parameters.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.AddWithValue("@Result", safeResult);
+ cmd.Parameters.AddWithValue("@ErrorCode", safeErrorCode);
+
+ conn.Open();
+ cmd.ExecuteNonQuery();
+ }
+ }
+
+ // 성공 로그 출력 및 true 반환
+ System.Diagnostics.Debug.WriteLine(string.Format(
+ "[DB 저장 로그] S/N: {0} | 각인번호: {1} | QR: {2} | Maker: {3} | Model: {4} | Line: {5} | LotNo: {6} | Vol_1: {7:F2}V | Cur_1: {8:F2}mA | Res_1: {9:F3}kΩ | 결과: {10} | 시작:{11} | 종료:{12}",
+ serial, markedSerial, markingQr, safeMaker, safeModel, safeLine, safeLotNo, roundedVoltage, roundedCurrent, roundedResistance, result,
+ startTime.ToString("HH:mm:ss"), endTime.ToString("HH:mm:ss")));
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[DB SaveResult] DB 저장 실패 (메시지: " + ex.Message + ")");
+ throw; // 예외 원인을 상위로 전파하여 UI 상태창에 구체적인 오류 원인(ex.Message)이 그대로 노출되도록 함
+ }
+ }
+ ///
+ /// 지정한 LotNo로 기존에 저장 완료(PASS)된 제품들의 개수를 카운트하여 다음 각인 일련번호를 구합니다.
+ ///
+ public int GetNextMarkingSequence(string lotNo)
+ {
+ if (_offlineMode)
+ {
+ return 1;
+ }
+
+ try
+ {
+ string query = "SELECT COUNT(*) FROM [dbo].[Marking] WHERE [Lot_No] = @LotNo AND ([Result] = 'PASS' OR [Result] = 'OK')";
+
+ using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
+ {
+ using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
+ {
+ cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
+ cmd.Parameters.AddWithValue("@LotNo", lotNo);
+ conn.Open();
+ int count = (int)cmd.ExecuteScalar();
+ return count + 1;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[DB GetNextMarkingSequence] 에러: " + ex.Message);
+ throw; // 예외를 상위로 전파하여 DB 장애 시 중복 순번 발급 방지
+ }
+ }
+ }
+}
diff --git a/marking_gui/Services/EquipmentMeasurementService.cs b/marking_gui/Services/EquipmentMeasurementService.cs
new file mode 100644
index 0000000..576060c
--- /dev/null
+++ b/marking_gui/Services/EquipmentMeasurementService.cs
@@ -0,0 +1,417 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace marking_gui.Services
+{
+ ///
+ /// DMM(전압+저항, .10) + DMM(전류, .11) + Power Supply(출력제어, .12) SCPI 계측기 연동 서비스.
+ /// Setting.ini 의 [DMM] / [CurrentDMM] / [PowerSupply] 섹션에서 연결 설정을 읽습니다.
+ ///
+ public sealed class EquipmentMeasurementService : IDisposable
+ {
+ // ── 설정 내부 데이터 클래스 ──────────────────────────────────────
+ private sealed class ScpiChannelSettings
+ {
+ public string Connection { get; set; } = string.Empty;
+ public string Host { get; set; } = string.Empty;
+ public int Port { get; set; } = 5025;
+ public string IdnMatch { get; set; } = string.Empty;
+ public string PortName { get; set; } = string.Empty;
+ public int BaudRate { get; set; } = 9600;
+ public string Terminator { get; set; } = "LF";
+ public string SetupCommand { get; set; } = string.Empty;
+ public string CleanupCommand { get; set; } = string.Empty;
+ public string ReadCommand { get; set; } = string.Empty;
+
+ public bool IsDisabled =>
+ string.IsNullOrWhiteSpace(Connection) ||
+ (Connection.ToUpperInvariant() is not ("LAN" or "TCP" or "ETHERNET" or "SERIAL" or "COM") &&
+ string.IsNullOrWhiteSpace(Host));
+
+ public string[] GetIdnMatches() =>
+ string.IsNullOrWhiteSpace(IdnMatch)
+ ? Array.Empty()
+ : IdnMatch.Split(',', StringSplitOptions.RemoveEmptyEntries);
+
+ public char GetTerminator() =>
+ Terminator.ToUpperInvariant() == "CR" ? '\r' : '\n';
+ }
+
+ // ── 필드 ────────────────────────────────────────────────────────
+ private int _timeoutMs = 5000;
+ private int _settleMs = 300;
+ private string _preBoardCommand = string.Empty;
+ private int _preBoardDelayMs = 2000;
+ private string _logoutCommand = string.Empty;
+ private string _resistanceReadCommand = "MEAS:RES?";
+
+ private ScpiChannelSettings _voltageDmm; // 전압 측정 전용 DMM 계측기
+ private ScpiChannelSettings _currentDmm; // 전류 측정 전용 DMM 계측기
+ private ScpiChannelSettings _resistanceDmm; // 저항 측정 전용 DMM 계측기
+ private ScpiChannelSettings _psu; // 출력 제어 Power Supply (전원 및 상시전원 인가)
+
+ private IScpiClient _voltageDmmClient;
+ private IScpiClient _currentDmmClient;
+ private IScpiClient _resistanceDmmClient;
+ private IScpiClient _psuClient;
+
+ private bool _disposed;
+
+ // ── 생성자 ──────────────────────────────────────────────────────
+ public EquipmentMeasurementService(string iniPath)
+ {
+ LoadSettings(iniPath);
+ }
+
+ private IScpiClient GetPsuClient()
+ {
+ if (_psuClient == null && !_psu.IsDisabled)
+ {
+ _psuClient = CreateClient("PSU", _psu);
+ }
+ return _psuClient;
+ }
+
+ private IScpiClient GetVoltageClient()
+ {
+ if (_voltageDmmClient == null && !_voltageDmm.IsDisabled)
+ {
+ _voltageDmmClient = CreateClient("VOLTAGE_DMM", _voltageDmm);
+ }
+ return _voltageDmmClient;
+ }
+
+ private IScpiClient GetCurrentDmmClient()
+ {
+ if (_currentDmmClient == null && !_currentDmm.IsDisabled)
+ {
+ _currentDmmClient = CreateClient("CURRENT_DMM", _currentDmm);
+ }
+ return _currentDmmClient;
+ }
+
+ private IScpiClient GetResistanceDmmClient()
+ {
+ if (_resistanceDmmClient == null && !_resistanceDmm.IsDisabled)
+ {
+ _resistanceDmmClient = CreateClient("RESISTANCE_DMM", _resistanceDmm);
+ }
+ // 저항 전용 DMM이 비활성화되어 있는 경우, 전압 DMM 클라이언트를 대체 사용
+ return _resistanceDmmClient ?? GetVoltageClient();
+ }
+
+ // ── 설정 로드 ────────────────────────────────────────────────────
+ private void LoadSettings(string iniPath)
+ {
+ var values = ParseIni(iniPath);
+
+ _timeoutMs = GetInt(values, "Timeout", 5000);
+ _settleMs = GetInt(values, "SettleMilliseconds", 300);
+ _preBoardCommand = GetString(values, "PreBoardCommand", string.Empty);
+ _preBoardDelayMs = GetInt(values, "PreBoardDelayMilliseconds", 2000);
+ _logoutCommand = GetString(values, "LogoutCommand", string.Empty);
+ _resistanceReadCommand = GetString(values, "ResistanceReadCommand", "MEAS:RES?");
+
+ _voltageDmm = LoadChannel(values, "Voltage", "MEAS:VOLT:DC?", "34460,34461,34465,34470");
+ _currentDmm = LoadChannel(values, "CurrentDMM", "MEAS:CURR:DC?", "34460,34461,34465,34470");
+ _resistanceDmm = LoadChannel(values, "ResistanceDMM", "MEAS:RES?", "34460,34461,34465,34470");
+ _psu = LoadChannel(values, "Current", string.Empty, "E362");
+ }
+
+ private static ScpiChannelSettings LoadChannel(
+ Dictionary values,
+ string prefix,
+ string defaultReadCommand,
+ string defaultIdnMatch)
+ {
+ return new ScpiChannelSettings
+ {
+ Connection = GetString(values, prefix + "Connection", string.Empty),
+ Host = GetString(values, prefix + "Host", string.Empty),
+ Port = GetInt(values, prefix + "Port", 5025),
+ IdnMatch = GetString(values, prefix + "IdnMatch", defaultIdnMatch),
+ PortName = GetString(values, prefix + "PortName", string.Empty),
+ BaudRate = GetInt(values, prefix + "BaudRate", 9600),
+ Terminator = GetString(values, prefix + "Terminator", "LF"),
+ SetupCommand = GetString(values, prefix + "SetupCommand", string.Empty),
+ CleanupCommand = GetString(values, prefix + "CleanupCommand", string.Empty),
+ ReadCommand = GetString(values, prefix + "ReadCommand", defaultReadCommand),
+ };
+ }
+
+ // ── Public API ───────────────────────────────────────────────────
+
+ ///
+ /// 전압 DMM + 저항 DMM + 전류 DMM(또는 PSU) 독립 계측기 3-장비 측정 시퀀스.
+ ///
+ public async Task<(double Voltage, double Current, double Resistance)> MeasureAsync(Action onUpdate = null)
+ {
+ var log = new StringBuilder();
+ IScpiClient psuClient = GetPsuClient();
+ IScpiClient voltageClient = GetVoltageClient();
+ IScpiClient resistanceClient = GetResistanceDmmClient();
+ IScpiClient currentDmmClient = GetCurrentDmmClient();
+
+ try
+ {
+ if (psuClient == null)
+ throw new InvalidOperationException("PSU 파워서플라이 장치가 설정되지 않았거나 비활성화 상태입니다.");
+ if (voltageClient == null)
+ throw new InvalidOperationException("전압 측정 DMM 장치가 설정되지 않았거나 비활성화 상태입니다.");
+
+ // 1. 파워서플라이(PSU) CH1 출력 ON
+ log.AppendLine($"> PSU CONNECT ({_psu.Host})");
+ string psuSetup = !string.IsNullOrWhiteSpace(_psu.SetupCommand) ? _psu.SetupCommand : "OUTP ON, (@1)";
+ log.AppendLine($"> PSU SETUP: {psuSetup}");
+ await SendCommandListAsync(psuClient, psuSetup);
+
+ // settle 지연 대기
+ await Task.Delay(_settleMs).ConfigureAwait(false);
+
+ // 2. 전압 측정 (전압 전용 계측기에서 MEAS:VOLT:DC? 로 측정)
+ log.AppendLine($"> 전압 DMM CONNECT ({_voltageDmm.Host})");
+ if (!string.IsNullOrWhiteSpace(_voltageDmm.SetupCommand))
+ {
+ log.AppendLine($"> 전압 DMM SETUP: {_voltageDmm.SetupCommand}");
+ await SendCommandListAsync(voltageClient, _voltageDmm.SetupCommand);
+ }
+
+ string voltageReadCmd = !string.IsNullOrWhiteSpace(_voltageDmm.ReadCommand) ? _voltageDmm.ReadCommand : "MEAS:VOLT:DC?";
+ log.AppendLine($"> 전압 DMM SEND: {voltageReadCmd}");
+ string vResponse = await voltageClient.QueryAsync(voltageReadCmd);
+ log.AppendLine($" 응답: {vResponse.Trim()}");
+ double voltage = (double)ExtractFirstDecimal(vResponse, "VOLTAGE");
+
+ // 실시간 전압 업데이트
+ onUpdate?.Invoke(voltage, null, null);
+
+ // 3. 저항 측정 (저항 전용 계측기에서 MEAS:RES? 로 측정)
+ log.AppendLine($"> 저항 DMM CONNECT ({_resistanceDmm.Host ?? _voltageDmm.Host})");
+ if (resistanceClient != null && !string.IsNullOrWhiteSpace(_resistanceDmm.SetupCommand))
+ {
+ log.AppendLine($"> 저항 DMM SETUP: {_resistanceDmm.SetupCommand}");
+ await SendCommandListAsync(resistanceClient, _resistanceDmm.SetupCommand);
+ }
+
+ string resReadCmd = !string.IsNullOrWhiteSpace(_resistanceDmm.ReadCommand) ? _resistanceDmm.ReadCommand : _resistanceReadCommand;
+ log.AppendLine($"> 저항 DMM SEND: {resReadCmd}");
+ string rResponse = await (resistanceClient ?? voltageClient).QueryAsync(resReadCmd);
+ log.AppendLine($" 응답: {rResponse.Trim()}");
+ double rawResistance = (double)ExtractFirstDecimal(rResponse, "RESISTANCE");
+ double resistance = rawResistance / 1000.0; // Ohms -> kOhms 변환 (kΩ 단위 표출)
+
+ // 실시간 저항 업데이트
+ onUpdate?.Invoke(voltage, null, resistance);
+
+ // 4. 전류 측정 (파워서플라이 CH1에서 MEAS:CURR? (@1) 로 측정)
+ log.AppendLine($"> PSU 전류 측정 CONNECT ({_psu.Host})");
+ string currentReadCmd = !string.IsNullOrWhiteSpace(_psu.ReadCommand) ? _psu.ReadCommand : "MEAS:CURR? (@1)";
+ log.AppendLine($"> PSU SEND: {currentReadCmd}");
+ string aResponse = await psuClient.QueryAsync(currentReadCmd);
+ log.AppendLine($" 응답: {aResponse.Trim()}");
+ double rawCurrent = (double)ExtractFirstDecimal(aResponse, "CURRENT");
+ double current = rawCurrent * 1000.0; // Amperes -> mA 변환
+
+ // 실시간 전류 및 최종 업데이트
+ onUpdate?.Invoke(voltage, current, resistance);
+
+ return (voltage, current, resistance);
+ }
+ finally
+ {
+ // 5. 파워서플라이 CH1 출력 OFF
+ if (psuClient != null)
+ {
+ try
+ {
+ string psuCleanup = !string.IsNullOrWhiteSpace(_psu.CleanupCommand) ? _psu.CleanupCommand : "OUTP OFF, (@1)";
+ log.AppendLine($"> PSU CLEANUP: {psuCleanup}");
+ await SendCommandListAsync(psuClient, psuCleanup);
+ }
+ catch (Exception ex)
+ {
+ log.AppendLine($"> PSU OUTP OFF FAILED: {ex.Message}");
+ }
+ }
+ LoggerService.Info("[Equipment] 독립 계측기 측정 시퀀스 로그:\n" + log);
+ }
+ }
+
+ /// 검사 전 보드 초기화 커맨드 실행 (PreBoardCommand).
+ public async Task RunPreBoardCommandAsync()
+ {
+ if (_psu.IsDisabled)
+ {
+ LoggerService.Warn("[Equipment] PreBoard 커맨드 실행 불가: Power Supply 설정이 비활성화되어 있거나 올바르지 않습니다.");
+ throw new InvalidOperationException("Power Supply 장치가 비활성화 상태이거나 [PowerSupply] 설정이 올바르지 않습니다.");
+ }
+
+ if (string.IsNullOrWhiteSpace(_preBoardCommand))
+ {
+ LoggerService.Warn("[Equipment] PreBoard 커맨드 실행 불가: Setting.ini 에 PreBoardCommand 설정이 없습니다.");
+ throw new InvalidOperationException("Setting.ini [Equipment] 섹션의 PreBoardCommand 설정이 비어있습니다.");
+ }
+
+ var client = GetPsuClient();
+ if (client == null)
+ {
+ LoggerService.Error("[Equipment] PreBoard 커맨드 실행 불가: Power Supply 클라이언트 생성 실패");
+ throw new InvalidOperationException("Power Supply 클라이언트를 생성할 수 없습니다 (연동 설식을 확인하세요).");
+ }
+
+ LoggerService.Info($"[Equipment] PreBoard 커맨드 실행 시작: {_preBoardCommand}");
+ await SendCommandListAsync(client, _preBoardCommand);
+ await Task.Delay(_preBoardDelayMs).ConfigureAwait(false);
+ LoggerService.Info("[Equipment] PreBoard 커맨드(상시전원 ON) 실행 완료");
+ }
+
+ /// 로그아웃 시 보드 전원 차단 (LogoutCommand).
+ public async Task RunLogoutCommandAsync()
+ {
+ if (_psu.IsDisabled || string.IsNullOrWhiteSpace(_logoutCommand))
+ {
+ LoggerService.Warn("[Equipment] Logout 커맨드 실행 스킵: Power Supply 비활성화 상태이거나 LogoutCommand 설정이 없습니다.");
+ return;
+ }
+
+ var client = GetPsuClient();
+ if (client == null)
+ {
+ LoggerService.Error("[Equipment] Logout 커맨드 실행 실패: Power Supply 클라이언트 생성 불가");
+ return;
+ }
+
+ LoggerService.Info($"[Equipment] Logout 커맨드 실행: {_logoutCommand}");
+ await SendCommandListAsync(client, _logoutCommand);
+ }
+
+ // ── 내부 측정 헬퍼 ──────────────────────────────────────────────
+ private async Task ReadChannelAsync(
+ string label,
+ ScpiChannelSettings ch,
+ StringBuilder log)
+ {
+ if (ch.IsDisabled)
+ {
+ log.AppendLine($"> {label} SKIP: Setting.ini 에서 비활성화됨");
+ return 0.0;
+ }
+
+ if (string.IsNullOrWhiteSpace(ch.ReadCommand))
+ throw new InvalidOperationException($"Setting.ini [{label}] ReadCommand 값을 확인하세요.");
+
+ var client = label == "VOLTAGE" ? GetVoltageClient() : GetPsuClient();
+ if (client == null) throw new InvalidOperationException($"{label} 장치 클라이언트를 생성할 수 없습니다.");
+ return (double)await ReadChannelWithClientAsync(label, ch, client, log);
+ }
+
+ private static async Task ReadChannelWithClientAsync(
+ string label,
+ ScpiChannelSettings ch,
+ IScpiClient client,
+ StringBuilder log,
+ bool skipSetupCommand = false)
+ {
+ log.AppendLine($"> {label} CONNECT OK");
+
+ if (!skipSetupCommand && !string.IsNullOrWhiteSpace(ch.SetupCommand))
+ {
+ log.AppendLine($"> {label} SETUP: {ch.SetupCommand}");
+ await SendCommandListAsync(client, ch.SetupCommand);
+ }
+
+ log.AppendLine($"> {label} READ: {ch.ReadCommand}");
+ string response = await client.QueryAsync(ch.ReadCommand);
+ log.AppendLine($" 응답: {response.Trim()}");
+
+ return ExtractFirstDecimal(response, label);
+ }
+
+ private IScpiClient CreateClient(string label, ScpiChannelSettings ch)
+ {
+ string conn = ch.Connection.ToUpperInvariant();
+ if (conn is "LAN" or "TCP" or "ETHERNET")
+ {
+ return new TcpScpiClient(ch.Host, ch.Port, _timeoutMs, ch.GetIdnMatches(), ch.GetTerminator());
+ }
+ throw new InvalidOperationException(
+ $"{label} 장비 연결 방식 '{ch.Connection}' 은 지원되지 않습니다. LAN/TCP 만 지원됩니다.");
+ }
+
+ private static async Task SendCommandListAsync(IScpiClient client, string commands)
+ {
+ foreach (var cmd in commands.Split(';', StringSplitOptions.RemoveEmptyEntries))
+ {
+ string trimmed = cmd.Trim();
+ if (string.IsNullOrWhiteSpace(trimmed)) continue;
+ // (@2) 채널에 OUTP OFF 명령은 보호 대상 — 실수로 보드 전원 차단 방지
+ if (Regex.IsMatch(trimmed, @"^\s*OUTP(?:UT)?\s+OFF\s*,?\s*\(@2\)\s*$", RegexOptions.IgnoreCase))
+ continue;
+
+ await client.SendAsync(trimmed);
+ await Task.Delay(100).ConfigureAwait(false);
+ }
+ }
+
+ private static decimal ExtractFirstDecimal(string response, string label)
+ {
+ if (string.IsNullOrWhiteSpace(response))
+ throw new InvalidOperationException($"[{label}] 계측기 응답이 비어있습니다 (응답 수신 실패).");
+
+ var match = Regex.Match(response, @"[-+]?\d+(?:\.\d+)?(?:[Ee][-+]?\d+)?");
+ if (!match.Success)
+ throw new InvalidOperationException($"[{label}] 수치 파싱 실패. 원본 응답: [{response.Trim()}]");
+
+ if (!decimal.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal val))
+ throw new InvalidOperationException($"[{label}] 수치 변환 실패 (값: '{match.Value}'). 원본 응답: [{response.Trim()}]");
+
+ return val;
+ }
+
+ // ── INI 파서 ────────────────────────────────────────────────────
+ private static Dictionary ParseIni(string path)
+ {
+ var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (!File.Exists(path)) return result;
+
+ foreach (var raw in File.ReadAllLines(path))
+ {
+ string line = raw.Trim();
+ if (string.IsNullOrEmpty(line) || line.StartsWith(";") || line.StartsWith("[")) continue;
+ int eq = line.IndexOf('=');
+ if (eq <= 0) continue;
+ string key = line.Substring(0, eq).Trim();
+ string value = line.Substring(eq + 1).Trim();
+ result[key] = value;
+ }
+
+ return result;
+ }
+
+ private static string GetString(Dictionary d, string key, string def)
+ => d.TryGetValue(key, out var v) && !string.IsNullOrWhiteSpace(v) ? v : def;
+
+ private static int GetInt(Dictionary d, string key, int def)
+ => d.TryGetValue(key, out var v) && int.TryParse(v, out int n) ? n : def;
+
+ // ── IDisposable ──────────────────────────────────────────────────
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ try { _voltageDmmClient?.Dispose(); } catch { }
+ try { _currentDmmClient?.Dispose(); } catch { }
+ try { _resistanceDmmClient?.Dispose(); } catch { }
+ try { _psuClient?.Dispose(); } catch { }
+ _voltageDmmClient = null;
+ _currentDmmClient = null;
+ _resistanceDmmClient = null;
+ _psuClient = null;
+ }
+ }
+}
diff --git a/marking_gui/Services/EzdMarkingService.cs b/marking_gui/Services/EzdMarkingService.cs
new file mode 100644
index 0000000..09fa50f
--- /dev/null
+++ b/marking_gui/Services/EzdMarkingService.cs
@@ -0,0 +1,336 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using marking_gui.Models;
+
+namespace marking_gui.Services
+{
+ public class EzdMarkingService : IMarkingService
+ {
+ private bool _isInitialized = false;
+ private IntPtr _markingHandle = IntPtr.Zero; // 마킹 장치 기기 핸들 포인터
+ private string _ezdFolderPath = string.Empty;
+ private MarkingConfigService _config;
+ private string _lastInitErrorMessage = "초기화가 진행되지 않았습니다.";
+
+ public string LastInitErrorMessage => _lastInitErrorMessage;
+
+ // ==========================================
+ // MarkEzdWrapper.dll P/Invoke 선언부
+ // ==========================================
+
+ [DllImport("MarkEzdWrapper.dll", EntryPoint = "InitMarkEzd", CallingConvention = CallingConvention.Cdecl)]
+ private static extern IntPtr InitMarkEzd();
+
+ [DllImport("MarkEzdWrapper.dll", EntryPoint = "Init", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+ private static extern int Init(IntPtr hHandle, string strEzCadPath, IntPtr hOwenWnd);
+
+ [DllImport("MarkEzdWrapper.dll", EntryPoint = "LoadEzdFile", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)]
+ private static extern int LoadEzdFile(IntPtr hHandle, string strEzCadFileName);
+
+ [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);
+
+ [DllImport("MarkEzdWrapper.dll", EntryPoint = "ReadPort", CallingConvention = CallingConvention.Cdecl)]
+ private static extern int ReadPort(IntPtr hHandle, ref ushort Data);
+
+ [DllImport("MarkEzdWrapper.dll", EntryPoint = "WritePort", CallingConvention = CallingConvention.Cdecl)]
+ private static extern int WritePort(IntPtr hHandle, ushort Data);
+
+ [DllImport("MarkEzdWrapper.dll", EntryPoint = "Close", CallingConvention = CallingConvention.Cdecl)]
+ private static extern int Close(IntPtr hHandle);
+
+ public bool Initialize(string configPath, IntPtr windowHandle)
+ {
+ try
+ {
+ _lastInitErrorMessage = string.Empty;
+ _config = new MarkingConfigService();
+ _ezdFolderPath = _config.EzdFolderPath;
+
+ if (string.IsNullOrEmpty(_ezdFolderPath))
+ {
+ _lastInitErrorMessage = "Marking.ini 내 EzdFolderPath 경로가 비어 있습니다.";
+ _isInitialized = false;
+ return false;
+ }
+
+ if (!Directory.Exists(_ezdFolderPath))
+ {
+ _lastInitErrorMessage = string.Format("EzCad 폴더가 존재하지 않습니다: {0} (Marking.ini 설정 확인 필요)", _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)
+ {
+ _isInitialized = false;
+ _lastInitErrorMessage = "InitMarkEzd() 실행 결과 생성된 핸들 포인터가 Zero(Null)입니다. 마킹 보드 및 드라이버 연결 상태를 점검하세요.";
+ 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;
+ }
+
+ 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)
+ {
+ _isInitialized = false;
+ _lastInitErrorMessage = "마킹 컨트롤 보드 연동 예외 발생: " + ex.Message;
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 초기화 예외 발생: " + ex.Message);
+ return false;
+ }
+ }
+
+ [System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions]
+ [System.Security.SecurityCritical]
+ public MarkingResult RequestMarking(MarkingRequest request)
+ {
+ if (!_isInitialized || _markingHandle == IntPtr.Zero)
+ {
+ return new MarkingResult
+ {
+ Success = false,
+ ErrorCode = -10,
+ ErrorMessage = "LMC 마킹 디바이스가 연결 및 초기화되지 않았습니다. 상세 원인: " + _lastInitErrorMessage,
+ MarkedAt = DateTime.Now
+ };
+ }
+
+ try
+ {
+ // 1. 도안 파일 경로 확인
+ 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))
+ {
+ // 최종 확인 실패 시 에러
+ return new MarkingResult
+ {
+ Success = false,
+ ErrorCode = -11,
+ ErrorMessage = string.Format("도안 파일(.ezd)이 존재하지 않습니다: {0}", ezdFile),
+ MarkedAt = DateTime.Now
+ };
+ }
+
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 도안 파일 로드 시작: " + ezdPath);
+ string absoluteEzdPath = Path.GetFullPath(ezdPath);
+ int loadResult = LoadEzdFile(_markingHandle, absoluteEzdPath); // 획득한 핸들 전달
+ if (loadResult != 0)
+ {
+ return new MarkingResult
+ {
+ Success = false,
+ ErrorCode = loadResult,
+ ErrorMessage = string.Format("도안 파일(.ezd) 로드 실패. (경로: {0}, LoadEzdFile 에러코드: {1})", absoluteEzdPath, loadResult),
+ MarkedAt = DateTime.Now
+ };
+ }
+
+ // 1-1. 도안 파일(.ezd) 내부의 고정 텍스트 오브젝트(도면품번, HKMC 품번)를 직접 읽어오기
+ string ezdDrawingNo = GetTextFromEzd(_config.DrawingNoTag);
+ string ezdHkmcNo = GetTextFromEzd(_config.HkmcNoTag);
+
+ if (!string.IsNullOrWhiteSpace(ezdDrawingNo))
+ {
+ request.DrawingNo = ezdDrawingNo;
+ }
+ if (!string.IsNullOrWhiteSpace(ezdHkmcNo))
+ {
+ request.HkmcNo = ezdHkmcNo;
+ }
+
+ // 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);
+
+ // 2. 도안 내 변수 태그 치환 (사양서 4대 태그: S/N, DMC, 도면품번, HKMC품번)
+ ChangeTextInEzd(_config.SerialTag, request.Serial);
+ ChangeTextInEzd(_config.QrCodeTag, request.QrString);
+ ChangeTextInEzd(_config.DrawingNoTag, request.DrawingNo);
+ ChangeTextInEzd(_config.HkmcNoTag, request.HkmcNo);
+
+ 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));
+
+ // 3. 레이저 마킹 시작 (획득한 핸들 전달)
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 레이저 마킹 기계 동작 시작...");
+ int markResult = Mark(_markingHandle);
+
+ if (markResult == 0)
+ {
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 동작 완료 (성공)");
+ return new MarkingResult
+ {
+ Success = true,
+ ErrorCode = 0,
+ ErrorMessage = "마킹 완료",
+ MarkedAt = DateTime.Now
+ };
+ }
+ else
+ {
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 기계 동작 에러 발생: " + markResult);
+ 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
+ {
+ Success = false,
+ ErrorCode = -99,
+ ErrorMessage = "마킹 처리 중 예외 발생: " + ex.Message,
+ MarkedAt = DateTime.Now
+ };
+ }
+ }
+
+ private bool _disposed = false;
+
+ private string GetTextFromEzd(string tagName)
+ {
+ if (string.IsNullOrWhiteSpace(tagName) || _markingHandle == IntPtr.Zero)
+ return string.Empty;
+
+ try
+ {
+ // 1차: Unicode (UTF-16) 시도
+ var sbW = new System.Text.StringBuilder(512);
+ int resW = GetTextByNameW(_markingHandle, tagName, sbW);
+ if (resW == 0 && sbW.Length > 0)
+ {
+ return sbW.ToString().Trim();
+ }
+
+ // 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)
+ {
+ return sbA.ToString().Trim();
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[EzdMarking] GetTextByName({tagName}) 읽기 예외: {ex.Message}");
+ }
+ return string.Empty;
+ }
+
+ private void ChangeTextInEzd(string tagName, string textNew)
+ {
+ if (string.IsNullOrWhiteSpace(tagName) || _markingHandle == IntPtr.Zero) return;
+ try
+ {
+ int resW = ChangeTextByNameW(_markingHandle, tagName, textNew);
+ if (resW != 0)
+ {
+ ChangeTextByNameA(_markingHandle, tagName, textNew);
+ }
+ }
+ catch
+ {
+ // Fallback
+ }
+ }
+
+ public void Shutdown()
+ {
+ try
+ {
+ if (_isInitialized && _markingHandle != IntPtr.Zero)
+ {
+ Close(_markingHandle);
+ _markingHandle = IntPtr.Zero;
+ _isInitialized = false;
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 컨트롤 디바이스 정상 종료 완료.");
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[EzdMarking] 디바이스 종료 중 예외: " + ex.Message);
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposed)
+ {
+ Shutdown();
+ _disposed = true;
+ }
+ }
+
+ ~EzdMarkingService()
+ {
+ Dispose(false);
+ }
+ }
+}
diff --git a/marking_gui/Services/IMarkingService.cs b/marking_gui/Services/IMarkingService.cs
new file mode 100644
index 0000000..60c9e11
--- /dev/null
+++ b/marking_gui/Services/IMarkingService.cs
@@ -0,0 +1,32 @@
+using System;
+using marking_gui.Models;
+
+namespace marking_gui.Services
+{
+ public interface IMarkingService : IDisposable
+ {
+ ///
+ /// 마킹 서비스 및 하드웨어를 초기화합니다.
+ ///
+ /// 설정 파일(Config.ini 등)의 경로
+ /// 초기화 성공 여부
+ bool Initialize(string configPath, IntPtr windowHandle);
+
+ ///
+ /// 지정한 매개변수로 마킹 작업을 요청합니다.
+ ///
+ /// 마킹 파라미터 묶음
+ /// 마킹 처리 결과
+ MarkingResult RequestMarking(MarkingRequest request);
+
+ ///
+ /// 마지막 초기화 실패 시의 상세 에러 메시지
+ ///
+ string LastInitErrorMessage { get; }
+
+ ///
+ /// 마킹 드라이버 및 리소스를 정상적으로 해제합니다.
+ ///
+ void Shutdown();
+ }
+}
diff --git a/marking_gui/Services/IScpiClient.cs b/marking_gui/Services/IScpiClient.cs
new file mode 100644
index 0000000..3c8d58d
--- /dev/null
+++ b/marking_gui/Services/IScpiClient.cs
@@ -0,0 +1,17 @@
+using System.Threading.Tasks;
+
+namespace marking_gui.Services
+{
+ ///
+ /// SCPI 계측기와 명령을 주고받는 클라이언트 인터페이스.
+ /// LAN(TCP), Serial 등 다양한 연결 방식에 대응합니다.
+ ///
+ public interface IScpiClient : System.IDisposable
+ {
+ /// 계측기에 SCPI 명령을 전송합니다 (응답 없음).
+ Task SendAsync(string command);
+
+ /// 계측기에 SCPI 명령을 전송하고 응답 문자열을 반환합니다.
+ Task QueryAsync(string command);
+ }
+}
diff --git a/marking_gui/Services/LoggerService.cs b/marking_gui/Services/LoggerService.cs
new file mode 100644
index 0000000..c3399e7
--- /dev/null
+++ b/marking_gui/Services/LoggerService.cs
@@ -0,0 +1,96 @@
+using System;
+using System.IO;
+
+namespace marking_gui.Services
+{
+ public static class LoggerService
+ {
+ private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
+ private static readonly object LockObj = new object();
+
+ static LoggerService()
+ {
+ try
+ {
+ if (!Directory.Exists(LogDirectory))
+ {
+ Directory.CreateDirectory(LogDirectory);
+ }
+
+ // 오래된 로그 정리 태스크 백그라운드 구동 (애플리케이션 시작 차단 방지)
+ System.Threading.Tasks.Task.Run(() => CleanUpOldLogs(30));
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("Log directory creation failed: " + ex.Message);
+ }
+ }
+
+ private static void CleanUpOldLogs(int retentionDays)
+ {
+ try
+ {
+ var dirInfo = new DirectoryInfo(LogDirectory);
+ if (!dirInfo.Exists) return;
+
+ DateTime threshold = DateTime.Now.AddDays(-retentionDays);
+ foreach (var file in dirInfo.GetFiles("*.log"))
+ {
+ try
+ {
+ if (file.LastWriteTime < threshold)
+ {
+ file.Delete();
+ System.Diagnostics.Debug.WriteLine($"[SRE Logger] 만료된 로그 파일 삭제 완료: {file.Name}");
+ }
+ }
+ catch (Exception fileEx)
+ {
+ System.Diagnostics.Debug.WriteLine($"[SRE Logger] 로그 파일 삭제 중 예외 (파일명: {file.Name}): {fileEx.Message}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[SRE Logger] 만료 로그 정리 중 예외 발생: " + ex.Message);
+ }
+ }
+
+ public static void Info(string message)
+ {
+ WriteLog("INFO", message);
+ }
+
+ public static void Warn(string message)
+ {
+ WriteLog("WARN", message);
+ }
+
+ public static void Error(string message, Exception ex = null)
+ {
+ string detail = message;
+ if (ex != null)
+ {
+ detail += string.Format("\n[Exception] {0}\n[StackTrace]\n{1}", ex.Message, ex.StackTrace);
+ }
+ WriteLog("ERROR", detail);
+ }
+
+ private static void WriteLog(string level, string message)
+ {
+ lock (LockObj)
+ {
+ try
+ {
+ string logFile = Path.Combine(LogDirectory, DateTime.Now.ToString("yyyy-MM-dd") + ".log");
+ string logLine = string.Format("[{0}] [{1}] {2}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), level, message) + Environment.NewLine;
+ File.AppendAllText(logFile, logLine);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("Writing log failed: " + ex.Message);
+ }
+ }
+ }
+ }
+}
diff --git a/marking_gui/Services/LoginServicesStub.cs b/marking_gui/Services/LoginServicesStub.cs
index af1dfa6..3e20547 100644
--- a/marking_gui/Services/LoginServicesStub.cs
+++ b/marking_gui/Services/LoginServicesStub.cs
@@ -9,7 +9,51 @@ namespace Housing.Services
// 1. DatabaseSettings
public class DatabaseSettings
{
- public string ConnectionString { get; set; } = string.Empty;
+ public string Db_Server { get; set; } = string.Empty;
+ public string Db_Name { get; set; } = string.Empty;
+ public string Db_User { get; set; } = string.Empty;
+ public string Db_Password { get; set; } = string.Empty;
+
+ private string _rawConnectionString = string.Empty;
+
+ public string ConnectionString
+ {
+ get
+ {
+ string connStr;
+ if (!string.IsNullOrWhiteSpace(Db_Server) &&
+ !string.IsNullOrWhiteSpace(Db_Name) &&
+ !string.IsNullOrWhiteSpace(Db_User) &&
+ !string.IsNullOrWhiteSpace(Db_Password))
+ {
+ connStr = $"Server={Db_Server};Database={Db_Name};User Id={Db_User};Password={Db_Password};";
+ }
+ else
+ {
+ connStr = _rawConnectionString;
+ }
+
+ if (!string.IsNullOrWhiteSpace(connStr) && connStr.IndexOf("TrustServerCertificate", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ if (!connStr.EndsWith(";")) connStr += ";";
+ connStr += "TrustServerCertificate=True;";
+ }
+
+ if (!string.IsNullOrWhiteSpace(connStr) &&
+ connStr.IndexOf("Connect Timeout", StringComparison.OrdinalIgnoreCase) < 0 &&
+ connStr.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0)
+ {
+ if (!connStr.EndsWith(";")) connStr += ";";
+ connStr += "Connect Timeout=3;";
+ }
+
+ return connStr;
+ }
+ set
+ {
+ _rawConnectionString = value;
+ }
+ }
public static DatabaseSettings Load(string path)
{
@@ -22,6 +66,12 @@ namespace Housing.Services
foreach (var line in lines)
{
var trimmed = line.Trim();
+
+ if (trimmed.StartsWith(";") || trimmed.StartsWith("#"))
+ {
+ continue;
+ }
+
if (trimmed.StartsWith("ConnectionString", StringComparison.OrdinalIgnoreCase))
{
var parts = trimmed.Split(new[] { '=' }, 2);
@@ -30,6 +80,38 @@ namespace Housing.Services
settings.ConnectionString = parts[1].Trim();
}
}
+ else if (trimmed.StartsWith("Db_Server", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ settings.Db_Server = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("Db_Name", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ settings.Db_Name = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("Db_User", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ settings.Db_User = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("Db_Password", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ settings.Db_Password = parts[1].Trim();
+ }
+ }
}
}
}
@@ -273,6 +355,7 @@ namespace Housing.Services
{
using (var cmd = new System.Data.SqlClient.SqlCommand(_loginSettings.Procedure, conn))
{
+ cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
cmd.CommandType = System.Data.CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@Operator", operatorId);
cmd.Parameters.AddWithValue("@Password", password);
@@ -304,6 +387,7 @@ namespace Housing.Services
const string query = "SELECT COUNT(*) FROM [Users] WHERE [OperatorId] = @Id AND [Password] = @Pw";
using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
{
+ cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
cmd.Parameters.AddWithValue("@Id", operatorId);
cmd.Parameters.AddWithValue("@Pw", password);
@@ -354,4 +438,89 @@ namespace Housing.Services
return false;
}
}
+
+ // 5. LoginAccount (For LoginOptions.json)
+ public class LoginAccount
+ {
+ public string Id { get; set; } = string.Empty;
+ public string Password { get; set; } = string.Empty;
+ }
+
+ // 6. LoginSelectionOptions (For LoginOptions.json)
+ public class LoginSelectionOptions
+ {
+ public List LoginAccounts { get; set; } = new List();
+ public List Makers { get; set; } = new List();
+ public List Models { get; set; } = new List();
+ public List Variant1Values { get; set; } = new List();
+ public List Variant2Values { get; set; } = new List();
+ public List LineValues { get; set; } = new List();
+ public List JigNoValues { get; set; } = new List();
+
+ public static LoginSelectionOptions Load(string path)
+ {
+ try
+ {
+ if (File.Exists(path))
+ {
+ var json = File.ReadAllText(path);
+ var options = new System.Text.Json.JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ var loaded = System.Text.Json.JsonSerializer.Deserialize(json, options);
+ if (loaded != null)
+ {
+ return loaded;
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("Failed to load LoginOptions: " + ex.Message);
+ }
+ return new LoginSelectionOptions();
+ }
+
+ public static void CreateDefaultFile(string path)
+ {
+ try
+ {
+ var dir = Path.GetDirectoryName(path);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ {
+ Directory.CreateDirectory(dir);
+ }
+
+ var defaultContent = @"{
+ ""loginAccounts"": [
+ {
+ ""id"": ""su"",
+ ""password"": ""su""
+ }
+ ],
+ ""makers"": [
+ ""AMO"",
+ ""ABC""
+ ],
+ ""models"": [
+ ""MODEL-1""
+ ],
+ ""variant1Values"": [],
+ ""variant2Values"": [],
+ ""lineValues"": [
+ ""LINE-1""
+ ],
+ ""jigNoValues"": [
+ ""JIG-1""
+ ]
+}";
+ File.WriteAllText(path, defaultContent);
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("Failed to create default LoginOptions file: " + ex.Message);
+ }
+ }
+ }
}
diff --git a/marking_gui/Services/MarkingConfigService.cs b/marking_gui/Services/MarkingConfigService.cs
new file mode 100644
index 0000000..2be1161
--- /dev/null
+++ b/marking_gui/Services/MarkingConfigService.cs
@@ -0,0 +1,259 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace marking_gui.Services
+{
+ public class MarkingConfigService
+ {
+ private readonly Dictionary _settings = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _tags = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public string EzdFolderPath
+ {
+ get
+ {
+ string path = GetSetting("EzdFolderPath", @"c:\EzCad\EasyCfg");
+
+ // 1. C:\EzCad\EasyCfg 및 설정 경로 체크, 없으면 실행 파일 기준 폴더 사용
+ string defaultFallback = Directory.Exists(@"c:\EzCad\EasyCfg") ? @"c:\EzCad\EasyCfg" : AppDomain.CurrentDomain.BaseDirectory;
+
+ // 1. 설정값이 비어있을 시 실행 파일 기준 폴더 사용
+ if (string.IsNullOrWhiteSpace(path))
+ {
+ return defaultFallback;
+ }
+
+ // 2. 상대 경로일 시 실행 파일 디렉토리 기준으로 절대 경로 병합
+ if (!Path.IsPathRooted(path))
+ {
+ return Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path));
+ }
+
+ // 3. 절대 경로이지만 타깃 경로가 실재하지 않는 경우 실행 파일 폴더로 Fallback
+ if (!Directory.Exists(path))
+ {
+ return defaultFallback;
+ }
+
+ return path;
+ }
+ }
+
+ public string LastSelectedEzdFile => GetSetting("LastSelectedEzdFile", string.Empty);
+
+ public void SaveLastSelectedEzdFile(string filePath)
+ {
+ try
+ {
+ _settings["LastSelectedEzdFile"] = filePath;
+ string baseDir = AppDomain.CurrentDomain.BaseDirectory;
+ string iniPath = Path.Combine(baseDir, "Setting.ini");
+
+ if (File.Exists(iniPath))
+ {
+ var lines = new List(File.ReadAllLines(iniPath));
+ bool found = false;
+ for (int i = 0; i < lines.Count; i++)
+ {
+ string trimmed = lines[i].Trim();
+ if (trimmed.StartsWith("LastSelectedEzdFile=", StringComparison.OrdinalIgnoreCase))
+ {
+ lines[i] = "LastSelectedEzdFile=" + filePath;
+ found = true;
+ break;
+ }
+ }
+
+ if (!found)
+ {
+ // [Settings] 섹션 아래에 추가
+ for (int i = 0; i < lines.Count; i++)
+ {
+ if (lines[i].Trim().Equals("[Settings]", StringComparison.OrdinalIgnoreCase))
+ {
+ lines.Insert(i + 1, "LastSelectedEzdFile=" + filePath);
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (found)
+ {
+ File.WriteAllLines(iniPath, lines);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[MarkingConfig] LastSelectedEzdFile 저장 중 에러: " + ex.Message);
+ }
+ }
+
+ public string SerialTag => GetTag("SerialTag", "SERIAL");
+ public string LotNoTag => GetTag("LotNoTag", "YYMMDD");
+ public string ProductTag => GetTag("ProductTag", "PRODUCT");
+ public string QrCodeTag => GetTag("QrCodeTag", "QRCODETAG");
+ public string LineTag => GetTag("LineTag", "LCODE");
+ public string ShiftTag => GetTag("ShiftTag", "BAN");
+ public string DrawingNoTag => GetTag("DrawingNoTag", "DRAWING_NO");
+ public string HkmcNoTag => GetTag("HkmcNoTag", "HKMC_NO");
+
+ public string DrawingNo => GetSetting("DrawingNo", string.Empty);
+ public string HkmcNo => GetSetting("HkmcNo", string.Empty);
+
+ public bool IsTestMode
+ {
+ get
+ {
+ string val = GetSetting("test", "false");
+ return bool.TryParse(val, out bool result) && result;
+ }
+ }
+
+ public MarkingConfigService()
+ {
+ LoadConfig();
+ }
+
+ private void LoadConfig()
+ {
+ try
+ {
+ // BaseDirectory에서 Setting.ini 로드 시도
+ string baseDir = AppDomain.CurrentDomain.BaseDirectory;
+ string iniPath = Path.Combine(baseDir, "Setting.ini");
+
+ // BaseDirectory에 없으면 한 단계 위 프로젝트 폴더에서도 탐색 (개발 편의성)
+ if (!File.Exists(iniPath))
+ {
+ string parentDir = Directory.GetParent(baseDir)?.Parent?.Parent?.FullName;
+ if (parentDir != null)
+ {
+ string devPath = Path.Combine(parentDir, "marking_gui", "Setting.ini");
+ if (File.Exists(devPath))
+ {
+ iniPath = devPath;
+ }
+ }
+ }
+
+ if (File.Exists(iniPath))
+ {
+ byte[] bytes = File.ReadAllBytes(iniPath);
+ string fileText;
+ if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
+ {
+ fileText = System.Text.Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3);
+ }
+ else
+ {
+ try
+ {
+ var strictUtf8 = new System.Text.UTF8Encoding(false, true);
+ fileText = strictUtf8.GetString(bytes);
+ }
+ catch
+ {
+ fileText = System.Text.Encoding.Default.GetString(bytes);
+ }
+ }
+
+ var lines = fileText.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
+ string currentSection = string.Empty;
+
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+ if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith(";"))
+ continue;
+
+ if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
+ {
+ currentSection = trimmed.Substring(1, trimmed.Length - 2).Trim();
+ continue;
+ }
+
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ var key = parts[0].Trim();
+ var val = parts[1].Trim();
+
+ if (currentSection.Equals("Settings", StringComparison.OrdinalIgnoreCase))
+ {
+ _settings[key] = val;
+ }
+ else if (currentSection.Equals("Tags", StringComparison.OrdinalIgnoreCase))
+ {
+ _tags[key] = val;
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[MarkingConfig] 설정 로드 중 에러: " + ex.Message);
+ }
+ }
+
+ private string GetSetting(string key, string defaultVal)
+ {
+ return _settings.TryGetValue(key, out string val) ? val : defaultVal;
+ }
+
+ private string GetTag(string key, string defaultVal)
+ {
+ return _tags.TryGetValue(key, out string val) ? val : defaultVal;
+ }
+
+ public int GetLastSequence(string lotNo)
+ {
+ if (string.IsNullOrWhiteSpace(lotNo)) return 0;
+ string key = "Seq_" + lotNo.Trim();
+ if (_settings.TryGetValue(key, out string valStr) && int.TryParse(valStr, out int seq))
+ {
+ return seq;
+ }
+ return 0;
+ }
+
+ public void SaveLastSequence(string lotNo, int sequence)
+ {
+ if (string.IsNullOrWhiteSpace(lotNo)) return;
+ try
+ {
+ string key = "Seq_" + lotNo.Trim();
+ _settings[key] = sequence.ToString();
+
+ string iniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
+ if (File.Exists(iniPath))
+ {
+ var lines = new List(File.ReadAllLines(iniPath));
+ bool found = false;
+ for (int i = 0; i < lines.Count; i++)
+ {
+ string trimmed = lines[i].Trim();
+ if (trimmed.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase))
+ {
+ lines[i] = string.Format("{0}={1}", key, sequence);
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ {
+ lines.Add(string.Format("{0}={1}", key, sequence));
+ }
+ File.WriteAllLines(iniPath, lines);
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine("[MarkingConfig] 순번 저장 실패: " + ex.Message);
+ }
+ }
+ }
+}
diff --git a/marking_gui/Services/MockDatabase.cs b/marking_gui/Services/MockDatabase.cs
deleted file mode 100644
index fd1844a..0000000
--- a/marking_gui/Services/MockDatabase.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System;
-using marking_gui.Models;
-
-namespace marking_gui.Services
-{
- public class MockDatabase
- {
- public Product GetProductBySerial(string serial)
- {
- if (string.IsNullOrEmpty(serial))
- return null;
-
- string searchSerial = serial.Trim().ToUpper();
-
- if (searchSerial == "PASS1234")
- {
- // 이전 검사 모두 합격
- return new Product(searchSerial, "PASS", "PASS", "PASS");
- }
- else if (searchSerial == "FAIL5678")
- {
- // 이전 검사 중 Calibration 단계 실패
- return new Product(searchSerial, "PASS", "FAIL", "PASS");
- }
-
- // 등록되지 않은 S/N
- return null;
- }
- }
-}
diff --git a/marking_gui/Services/MockMeasurer.cs b/marking_gui/Services/MockMeasurer.cs
deleted file mode 100644
index f52dc87..0000000
--- a/marking_gui/Services/MockMeasurer.cs
+++ /dev/null
@@ -1,75 +0,0 @@
-using System;
-using marking_gui.Models;
-
-namespace marking_gui.Services
-{
- public class MockMeasurer
- {
- private readonly Random _random = new Random();
-
- // 2초 동안 측정 중 요동칠 가상 데이터 생성
- public void GenerateNoiseValues(MeasurementLimits limits, out double voltage, out double current, out double resistance)
- {
- double midVoltage = (limits.MinVoltage + limits.MaxVoltage) / 2.0;
- double midCurrent = (limits.MinCurrent + limits.MaxCurrent) / 2.0;
- double midResistance = (limits.MinResistance + limits.MaxResistance) / 2.0;
-
- // 설정 범위 폭에 맞추어 ±50% 범위의 노이즈 생성
- double voltageSpan = limits.MaxVoltage - limits.MinVoltage;
- double currentSpan = limits.MaxCurrent - limits.MinCurrent;
- double resistanceSpan = limits.MaxResistance - limits.MinResistance;
-
- voltage = midVoltage + (_random.NextDouble() * voltageSpan - (voltageSpan / 2.0));
- current = midCurrent + (_random.NextDouble() * currentSpan - (currentSpan / 2.0));
- resistance = midResistance + (_random.NextDouble() * resistanceSpan - (resistanceSpan / 2.0));
- }
-
- // 최종 합/불 판정 측정값 결정 (80% 확률로 합격 범위, 20% 확률로 불합격 범위 값 생성)
- public void GenerateFinalValues(MeasurementLimits limits, out double voltage, out double current, out double resistance)
- {
- double voltageSpan = limits.MaxVoltage - limits.MinVoltage;
- double currentSpan = limits.MaxCurrent - limits.MinCurrent;
- double resistanceSpan = limits.MaxResistance - limits.MinResistance;
-
- if (_random.Next(0, 10) < 8)
- {
- // 합격 범위 내의 가상 측정값 생성
- voltage = limits.MinVoltage + (_random.NextDouble() * voltageSpan);
- current = limits.MinCurrent + (_random.NextDouble() * currentSpan);
- resistance = limits.MinResistance + (_random.NextDouble() * resistanceSpan);
- }
- else
- {
- // 불합격 범위 유도
- int errorType = _random.Next(0, 3);
- if (errorType == 0)
- {
- // 전압 에러 유도 (범위 폭의 50% 정도 벗어나도록 가/감산하여 음수 방지)
- voltage = _random.Next(0, 2) == 0
- ? (limits.MinVoltage - (voltageSpan * 0.5))
- : (limits.MaxVoltage + (voltageSpan * 0.5));
- current = limits.MinCurrent + (_random.NextDouble() * currentSpan);
- resistance = limits.MinResistance + (_random.NextDouble() * resistanceSpan);
- }
- else if (errorType == 1)
- {
- // 전류 에러 유도
- voltage = limits.MinVoltage + (_random.NextDouble() * voltageSpan);
- current = _random.Next(0, 2) == 0
- ? (limits.MinCurrent - (currentSpan * 0.5))
- : (limits.MaxCurrent + (currentSpan * 0.5));
- resistance = limits.MinResistance + (_random.NextDouble() * resistanceSpan);
- }
- else
- {
- // 저항 에러 유도
- voltage = limits.MinVoltage + (_random.NextDouble() * voltageSpan);
- current = limits.MinCurrent + (_random.NextDouble() * currentSpan);
- resistance = _random.Next(0, 2) == 0
- ? (limits.MinResistance - (resistanceSpan * 0.5))
- : (limits.MaxResistance + (resistanceSpan * 0.5));
- }
- }
- }
- }
-}
diff --git a/marking_gui/Services/MssqlDatabaseService.cs b/marking_gui/Services/MssqlDatabaseService.cs
deleted file mode 100644
index b8e682d..0000000
--- a/marking_gui/Services/MssqlDatabaseService.cs
+++ /dev/null
@@ -1,173 +0,0 @@
-using System;
-using System.Configuration;
-using marking_gui.Models;
-
-namespace marking_gui.Services
-{
- public class MssqlDatabaseService
- {
- private readonly string _connectionString;
-
- public MssqlDatabaseService()
- {
- try
- {
- // AppDomain.CurrentDomain.BaseDirectory 에서 Database.ini 로드 시도
- string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Database.ini");
- if (System.IO.File.Exists(iniPath))
- {
- var lines = System.IO.File.ReadAllLines(iniPath);
- foreach (var line in lines)
- {
- var trimmed = line.Trim();
- if (trimmed.StartsWith("ConnectionString", StringComparison.OrdinalIgnoreCase))
- {
- var parts = trimmed.Split(new[] { '=' }, 2);
- if (parts.Length == 2)
- {
- _connectionString = parts[1].Trim();
- break;
- }
- }
- }
- }
-
- // Database.ini 에 없을 경우 App.config 백업 사용
- if (string.IsNullOrEmpty(_connectionString))
- {
- var connSettings = ConfigurationManager.ConnectionStrings["MssqlConnection"];
- if (connSettings != null)
- {
- _connectionString = connSettings.ConnectionString;
- }
- else
- {
- _connectionString = "Server=localhost;Database=MarkingDb;Trusted_Connection=True;";
- }
- }
- }
- catch (Exception ex)
- {
- _connectionString = string.Empty;
- System.Diagnostics.Debug.WriteLine("설정 로드 중 예외 발생: " + ex.Message);
- }
- }
-
- public Product GetProductBySerial(string serial)
- {
- if (string.IsNullOrEmpty(serial))
- return null;
-
- string searchSerial = serial.Trim().ToUpper();
-
- // TODO: 사용자의 지시에 따라 실제 MSSQL DB 질의 쿼리는 아직 작성하지 않고 대기합니다.
- // 추후 실제 연동 시 아래 SQL Connection 및 SELECT 쿼리를 구현할 수 있도록 준비만 해둡니다.
- /*
- string query = "SELECT SerialNumber, HousingResult, CalResult, EolResult FROM ProductResults WHERE UPPER(SerialNumber) = @Serial";
- using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
- {
- ...
- }
- */
-
- // 준비 단계 동안 임시로 기존 Mock 데이터를 반환하도록 구현 (Stub)
- if (searchSerial == "PASS1234")
- {
- return new Product(searchSerial, "PASS", "PASS", "PASS");
- }
- else if (searchSerial == "FAIL5678")
- {
- return new Product(searchSerial, "PASS", "FAIL", "PASS");
- }
-
- return null;
- }
-
- ///
- /// 최종 성능 검출 값을 데이터베이스에 저장합니다.
- /// 전압과 전류 수치는 3,2 포맷(DECIMAL(3, 2) 등)에 대응하기 위해 소수점 이하 2자리로 반올림하여 처리합니다.
- ///
- public bool SaveProductResult(
- string serial,
- string maker,
- string model,
- string variant1,
- string variant2,
- string operatorId,
- string line,
- string lotNo,
- string jigNo,
- double voltage,
- double current,
- double resistance,
- string result)
- {
- // 전압과 전류, 저항을 소수점 이하 2자리(3,2 포맷 규격)로 반올림 처리
- decimal roundedVoltage = Math.Round((decimal)voltage, 2);
- decimal roundedCurrent = Math.Round((decimal)current, 2);
- decimal roundedResistance = Math.Round((decimal)resistance, 2);
-
- // DB 컬럼 길이 한계에 맞춰서 문자열 안전하게 자르기 (Truncate)
- string safeMaker = string.IsNullOrEmpty(maker) ? "" : (maker.Length > 10 ? maker.Substring(0, 10) : maker);
- string safeModel = string.IsNullOrEmpty(model) ? "" : (model.Length > 10 ? model.Substring(0, 10) : model);
- string safeVariant1 = string.IsNullOrEmpty(variant1) ? "" : (variant1.Length > 10 ? variant1.Substring(0, 10) : variant1);
- string safeVariant2 = string.IsNullOrEmpty(variant2) ? "" : (variant2.Length > 10 ? variant2.Substring(0, 10) : variant2);
- string safeOperator = string.IsNullOrEmpty(operatorId) ? "" : (operatorId.Length > 10 ? operatorId.Substring(0, 10) : operatorId);
- string safeLine = string.IsNullOrEmpty(line) ? "" : (line.Length > 5 ? line.Substring(0, 5) : line);
- string safeLotNo = string.IsNullOrEmpty(lotNo) ? "" : (lotNo.Length > 8 ? lotNo.Substring(0, 8) : lotNo);
- string safeJigNo = string.IsNullOrEmpty(jigNo) ? "" : (jigNo.Length > 10 ? jigNo.Substring(0, 10) : jigNo);
-
- DateTime prodDate = DateTime.Now;
- // QR Code 조합 포맷 생성 (Maker;Model;Variant_1;Variant_2;Operator;Production_Date)
- string markingQr = string.Format("{0};{1};{2};{3};{4};{5}",
- safeMaker, safeModel, safeVariant1, safeVariant2, safeOperator, prodDate.ToString("yyyyMMddHHmmss"));
- if (markingQr.Length > 100)
- {
- markingQr = markingQr.Substring(0, 100);
- }
-
- // TODO: 실제 MSSQL DB 적재를 위해 SQL 명령문 구현 템플릿 준비
- /*
- string query = @"INSERT INTO [dbo].[Marking]
- ([IC_SN], [Marking_QR], [Maker], [Model], [Variant_1], [Variant_2], [Operator], [Production_Date], [Line], [Lot_No], [Jig_No],
- [PT_Vol_1], [PT_Vol_2], [PT_Vol_3], [PT_Current_1], [PT_Current_2], [PT_Current_3], [PT_Resistance_1], [PT_Resistance_2], [PT_Resistance_3], [Result])
- VALUES
- (@Serial, @Qr, @Maker, @Model, @Var1, @Var2, @Operator, @ProdDate, @Line, @LotNo, @JigNo,
- @Vol1, 0.00, 0.00, @Cur1, 0.00, 0.00, @Res1, 0.00, 0.00, @Result)";
-
- using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
- {
- using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
- {
- 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);
- cmd.Parameters.AddWithValue("@Var2", safeVariant2);
- cmd.Parameters.AddWithValue("@Operator", safeOperator);
- cmd.Parameters.AddWithValue("@ProdDate", prodDate);
- cmd.Parameters.AddWithValue("@Line", safeLine);
- cmd.Parameters.AddWithValue("@LotNo", safeLotNo);
- cmd.Parameters.AddWithValue("@JigNo", safeJigNo);
-
- cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Vol1", System.Data.SqlDbType.Decimal) { Precision = 3, Scale = 2, Value = roundedVoltage });
- cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Cur1", System.Data.SqlDbType.Decimal) { Precision = 3, Scale = 2, Value = roundedCurrent });
- cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Res1", System.Data.SqlDbType.Decimal) { Precision = 3, Scale = 2, Value = roundedResistance });
- cmd.Parameters.AddWithValue("@Result", result);
-
- conn.Open();
- cmd.ExecuteNonQuery();
- }
- }
- */
-
- // 데모 단계 로그 출력 및 성공 반환
- System.Diagnostics.Debug.WriteLine(string.Format(
- "[DB 저장 로그] S/N: {0} | QR: {1} | Maker: {2} | Model: {3} | Line: {4} | LotNo: {5} | Vol_1: {6:F2}V | Cur_1: {7:F2}A | Res_1: {8:F2}Ω | 결과: {9}",
- serial, markingQr, safeMaker, safeModel, safeLine, safeLotNo, roundedVoltage, roundedCurrent, roundedResistance, result));
-
- return true;
- }
- }
-}
diff --git a/marking_gui/Services/TcpScpiClient.cs b/marking_gui/Services/TcpScpiClient.cs
new file mode 100644
index 0000000..03cd772
--- /dev/null
+++ b/marking_gui/Services/TcpScpiClient.cs
@@ -0,0 +1,200 @@
+using System;
+using System.IO;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace marking_gui.Services
+{
+ ///
+ /// TCP(LAN) 소켓을 사용하여 SCPI 계측기와 통신하는 클라이언트.
+ /// Keysight DMM 34465A, Power Supply E36233A 등 LAN 장비에 사용합니다.
+ ///
+ public sealed class TcpScpiClient : IScpiClient, IDisposable
+ {
+ private readonly string _host;
+ private readonly int _port;
+ private readonly int _timeoutMs;
+ private readonly string[] _idnMatches;
+ private readonly byte _terminator;
+
+ private TcpClient _tcp;
+ private NetworkStream _stream;
+ private readonly SemaphoreSlim _networkLock = new SemaphoreSlim(1, 1);
+ private bool _disposed = false;
+
+ public TcpScpiClient(string host, int port, int timeoutMs, string[] idnMatches, char terminator = '\n')
+ {
+ _host = host;
+ _port = port;
+ _timeoutMs = timeoutMs;
+ _idnMatches = idnMatches ?? Array.Empty();
+ _terminator = (byte)terminator;
+ }
+
+ // ── 연결 (필요 시 자동) ────────────────────────────────────────────
+ private async Task EnsureConnectedAsync()
+ {
+ if (_tcp != null && _tcp.Connected) return;
+
+ await _networkLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ if (_tcp != null && _tcp.Connected) return;
+
+ CleanUpSocket();
+ _tcp = new TcpClient();
+ _tcp.SendTimeout = _timeoutMs;
+ _tcp.ReceiveTimeout = _timeoutMs;
+
+ // 연결 시도 시 CancellationToken을 사용해 OS 타임아웃 대기를 피함
+ using (var cts = new CancellationTokenSource(_timeoutMs))
+ {
+ var connectTask = _tcp.ConnectAsync(_host, _port);
+ var delayTask = Task.Delay(_timeoutMs, cts.Token);
+ var completedTask = await Task.WhenAny(connectTask, delayTask).ConfigureAwait(false);
+
+ if (completedTask == delayTask)
+ {
+ CleanUpSocket();
+ throw new TimeoutException($"SCPI 계측기 연결 대기 시간 초과 ({_host}:{_port})");
+ }
+
+ cts.Cancel(); // delayTask 취소
+ await connectTask; // 예외 전파 확인
+ }
+
+ _stream = _tcp.GetStream();
+
+ // IDN 검증
+ if (_idnMatches.Length > 0)
+ {
+ string idn = await QueryRawAsync("*IDN?").ConfigureAwait(false);
+ bool matched = false;
+ foreach (var m in _idnMatches)
+ {
+ if (!string.IsNullOrWhiteSpace(m) &&
+ idn.IndexOf(m, StringComparison.OrdinalIgnoreCase) >= 0)
+ {
+ matched = true;
+ break;
+ }
+ }
+ if (!matched)
+ throw new InvalidOperationException(
+ $"SCPI IDN 불일치: 기대=[{string.Join(",", _idnMatches)}] 실제=[{idn.Trim()}] ({_host}:{_port})");
+ }
+
+ LoggerService.Info($"[TcpScpi] 연결 완료 및 IDN 검증 성공: {_host}:{_port}");
+ }
+ catch (Exception ex)
+ {
+ CleanUpSocket();
+ LoggerService.Error($"[TcpScpi] 연결 중 예외 발생: {_host}:{_port}", ex);
+ throw;
+ }
+ finally
+ {
+ _networkLock.Release();
+ }
+ }
+
+ // ── IScpiClient ───────────────────────────────────────────────────
+ public async Task SendAsync(string command)
+ {
+ await EnsureConnectedAsync().ConfigureAwait(false);
+ await _networkLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ string trimmedCommand = (command ?? "").TrimEnd('\r', '\n');
+ byte[] data = Encoding.ASCII.GetBytes(trimmedCommand + (char)_terminator);
+ await _stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
+ await _stream.FlushAsync().ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ CleanUpSocket();
+ throw;
+ }
+ finally
+ {
+ _networkLock.Release();
+ }
+ }
+
+ public async Task QueryAsync(string command)
+ {
+ await EnsureConnectedAsync().ConfigureAwait(false);
+ await _networkLock.WaitAsync().ConfigureAwait(false);
+ try
+ {
+ return await QueryRawAsync(command).ConfigureAwait(false);
+ }
+ catch (Exception)
+ {
+ CleanUpSocket();
+ throw;
+ }
+ finally
+ {
+ _networkLock.Release();
+ }
+ }
+
+ private async Task QueryRawAsync(string command)
+ {
+ string trimmedCommand = (command ?? "").TrimEnd('\r', '\n');
+ byte[] data = Encoding.ASCII.GetBytes(trimmedCommand + (char)_terminator);
+ await _stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
+ await _stream.FlushAsync().ConfigureAwait(false);
+
+ // 응답 수신 (터미네이터 LF가 검출될 때까지 안전하게 수신)
+ using (var cts = new CancellationTokenSource(_timeoutMs))
+ {
+ var sb = new StringBuilder();
+ var buf = new byte[1024];
+ try
+ {
+ while (true)
+ {
+ int n = await _stream.ReadAsync(buf, 0, buf.Length, cts.Token).ConfigureAwait(false);
+ if (n == 0) break;
+ sb.Append(Encoding.ASCII.GetString(buf, 0, n));
+
+ // 버퍼의 마지막 바이트가 터미네이터(종단문자)인지 체크
+ if (buf[n - 1] == _terminator)
+ {
+ break;
+ }
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw new TimeoutException($"SCPI 명령 '{trimmedCommand}' 응답 수신 타임아웃 ({_timeoutMs}ms) ({_host}:{_port})");
+ }
+
+ return sb.ToString();
+ }
+ }
+
+ private void CleanUpSocket()
+ {
+ try { _stream?.Dispose(); } catch { }
+ try { _tcp?.Dispose(); } catch { }
+ _stream = null;
+ _tcp = null;
+ }
+
+ // ── IDisposable ───────────────────────────────────────────────────
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ CleanUpSocket();
+ _networkLock.Dispose();
+ }
+ }
+ }
+}
diff --git a/marking_gui/Setting.ini b/marking_gui/Setting.ini
new file mode 100644
index 0000000..50632e5
--- /dev/null
+++ b/marking_gui/Setting.ini
@@ -0,0 +1,113 @@
+; =================================================================
+; 성능 검사 모니터링 시스템 - 레이저 마킹기 연동 설정 파일
+; =================================================================
+
+[Settings]
+; 마킹 제어 라이브러리 및 드라이버(EzCad) 폴더 경로
+EzdFolderPath=c:\EzCad\EasyCfg
+
+; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출)
+test=false
+
+; 도면품번 (EZD 도안의 도면품번 태그에 채워질 값 — DrawingNoTag에 매핑)
+DrawingNo=
+
+; HKMC 품번 (EZD 도안의 HKMC 품번 태그에 채워질 값 — HkmcNoTag에 매핑)
+HkmcNo=
+
+
+[Tags]
+; 도안(.ezd) 파일 내의 텍스트 오브젝트 치환용 태그명 매핑 리스트
+; (도안에 정의된 오브젝트명과 동일하게 설정해야 정상 작동합니다)
+
+; 일련번호 (Serial Number) 치환 태그
+SerialTag=SERIAL
+
+; 작업 년월일 (Lot No) 치환 태그
+LotNoTag=YYMMDD
+
+; 제품 모델명 (Product Name) 치환 태그
+ProductTag=PRODUCT
+
+; DMC 치환 태그
+QrCodeTag=QRCODETAG
+
+; 도면품번 치환 태그 (EZD 도안 내 도면품번 오브젝트명)
+DrawingNoTag=DRAWING_NO
+
+; HKMC 품번 치환 태그 (EZD 도안 내 HKMC 품번 오브젝트명)
+HkmcNoTag=HKMC_NO
+
+
+; =================================================================
+; 계측기 연동 설정 (SCPI / LAN)
+; =================================================================
+
+[Equipment]
+; 계측기 통신 타임아웃 (ms)
+Timeout=5000
+
+; 측정 안정화 대기 시간 (ms) — Power Supply ON 후 이 시간만큼 대기 후 읽음
+SettleMilliseconds=2000
+
+; 검사 전 보드 전원 공급 커맨드 (세미콜론으로 구분)
+PreBoardCommand=VOLT 5, (@2); CURR 2, (@2); OUTP ON, (@2)
+PreBoardDelayMilliseconds=2000
+
+; 로그아웃 시 전원 차단 커맨드
+LogoutCommand=OUTP OFF, (@1)
+
+
+[DMM]
+; Digital Multimeter — 전압(Voltage) 측정 전용 계측기
+VoltageConnection=LAN
+VoltageHost=192.168.10.14
+VoltagePort=5025
+VoltageIdnMatch=34465A
+VoltageBaudRate=115200
+VoltageTerminator=LF
+VoltageSetupCommand=
+VoltageReadCommand=MEAS:VOLT:DC?
+
+
+[ResistanceDMM]
+; Digital Multimeter — 저항(Resistance) 측정 전용 계측기
+ResistanceDMMConnection=LAN
+ResistanceDMMHost=192.168.10.13
+ResistanceDMMPort=5025
+ResistanceDMMIdnMatch=34465A
+ResistanceDMMBaudRate=115200
+ResistanceDMMTerminator=LF
+ResistanceDMMReadCommand=MEAS:RES?
+
+
+[PowerSupply]
+; Power Supply — 전류(Current) 측정 및 상시전원(PreBoard)/테스트전원 출력 제어
+CurrentConnection=LAN
+CurrentHost=192.168.10.15
+CurrentPort=5025
+CurrentIdnMatch=E36233A
+CurrentBaudRate=9600
+CurrentTerminator=LF
+CurrentSetupCommand=OUTP ON, (@1)
+CurrentCleanupCommand=OUTP OFF, (@1)
+CurrentReadCommand=MEAS:CURR? (@1)
+
+
+; =================================================================
+; 계측 합격/불합격 판정 기준 범위 설정 (Limits)
+; =================================================================
+
+[Limits]
+; 전압 판정 기준 범위 (V)
+MinVoltage=1.00
+MaxVoltage=6.00
+
+; 전류 판정 기준 범위 (mA)
+MinCurrent=5.00
+MaxCurrent=10.00
+
+; 저항 판정 기준 범위 (kΩ)
+MinResistance=8.000
+MaxResistance=13.000
+
diff --git a/marking_gui/SettingsWindow.xaml b/marking_gui/SettingsWindow.xaml
index 33a5d4b..7b2c831 100644
--- a/marking_gui/SettingsWindow.xaml
+++ b/marking_gui/SettingsWindow.xaml
@@ -2,6 +2,7 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="기준 범위 설정" Height="290" Width="350"
+ Icon="pack://application:,,,/Marking.ico"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#F3F4F6" ShowInTaskbar="False">
diff --git a/marking_gui/SettingsWindow.xaml.cs b/marking_gui/SettingsWindow.xaml.cs
index f16f6eb..8f492d8 100644
--- a/marking_gui/SettingsWindow.xaml.cs
+++ b/marking_gui/SettingsWindow.xaml.cs
@@ -20,10 +20,10 @@ namespace marking_gui
txtMinVoltage.Text = Limits.MinVoltage.ToString("F1");
txtMaxVoltage.Text = Limits.MaxVoltage.ToString("F1");
- txtMinCurrent.Text = Limits.MinCurrent.ToString("F1");
- txtMaxCurrent.Text = Limits.MaxCurrent.ToString("F1");
- txtMinResistance.Text = Limits.MinResistance.ToString("F1");
- txtMaxResistance.Text = Limits.MaxResistance.ToString("F1");
+ txtMinCurrent.Text = Limits.MinCurrent.ToString("F2");
+ txtMaxCurrent.Text = Limits.MaxCurrent.ToString("F2");
+ txtMinResistance.Text = Limits.MinResistance.ToString("F3");
+ txtMaxResistance.Text = Limits.MaxResistance.ToString("F3");
}
private void btnSave_Click(object sender, RoutedEventArgs e)
diff --git a/marking_gui/StartupLoginWindow.xaml b/marking_gui/StartupLoginWindow.xaml
index 3c43103..920be5c 100644
--- a/marking_gui/StartupLoginWindow.xaml
+++ b/marking_gui/StartupLoginWindow.xaml
@@ -2,11 +2,17 @@
-
+
+
+
+