diff --git a/.agents/AGENTS.md b/.agents/AGENTS.md
new file mode 100644
index 0000000..be62410
--- /dev/null
+++ b/.agents/AGENTS.md
@@ -0,0 +1,4 @@
+# 프로젝트 개발 지침
+
+- **개발 전 단계**: 작업이나 구현 계획을 세우기 전, 반드시 `ob/` 폴더 내에 있는 시스템 설계 문서 및 관련 문서들을 상세히 읽어 요구사항과 설계 방향을 확인해야 합니다.
+- **개발 후 단계**: 코드 변경 또는 신규 기능 개발 완료 후, 작업 결과와 관련 설계 변경점을 `ob/` 폴더 내의 해당 문서에 성실히 업데이트하고 기록해야 합니다.
diff --git a/DB/dbo.EOL_Schema.sql b/DB/dbo.EOL_Schema.sql
index b20f827..3b53df3 100644
--- a/DB/dbo.EOL_Schema.sql
+++ b/DB/dbo.EOL_Schema.sql
@@ -1,10 +1,11 @@
-- ============================================================================
-- Project: NE1aW 온압센서
--- Document: dbo.EOL Table Schema (Simplified Version)
+-- Document: dbo.EOL Table Schema (Latest Version)
-- Author: Antigravity AI
--- Date: 2026-05-28
--- Description: 사용자 요구사항(ID, 좌/우, SPEC UL, SPEC LL, 누설값, 판정)에 맞추어 단순화된 스키마.
--- 재검사 이력 추적을 위해 Seq(PK) 및 InspectDateTime(일시) 컬럼 추가.
+-- Date: 2026-06-24
+-- Description: 개편된 로그인 사양 및 16개 컬럼 결과 저장 구조에 맞춘 스키마.
+-- 제품 일련번호(IC_SN)를 주 식별자로 사용하며,
+-- 동일 제품 재검사 시 MERGE 문을 이용해 UPSERT 처리됩니다.
-- ============================================================================
-- 1. 데이터베이스 존재 여부 확인 및 자동 생성
@@ -26,57 +27,78 @@ BEGIN
END
GO
--- 3. 단순화된 테이블 생성
+-- 3. 개편된 테이블 생성
CREATE TABLE [dbo].[EOL] (
- [InspectDate] NVARCHAR(10) NOT NULL, -- 검사 날짜 (YYYY-MM-DD)
- [InspectTime] NVARCHAR(8) NOT NULL, -- 검사 시간 (HH:MM:SS)
- [ID] NVARCHAR(30) NOT NULL, -- 센서 ID (ProductId) (기본키)
- [Channel] NVARCHAR(10) NOT NULL, -- 좌 / 우 (LEFT / RIGHT)
- [SpecUL] NVARCHAR(10) NOT NULL, -- SPEC UL
- [SpecLL] NVARCHAR(10) NOT NULL, -- SPEC LL
- [LeakValue] NVARCHAR(20) NOT NULL, -- 누설 측정값 (MeasuredValue)
- [Result] NVARCHAR(5) NOT NULL, -- 판정 결과 (OK / NG)
+ [IC_SN] NVARCHAR(10) NOT NULL, -- 제품의 최종 조합 ID (ProductId) (기본키)
+ [PCB_Barcode] NVARCHAR(10) NOT NULL, -- 고유 식별용 바코드 정보 (UK)
+ [Maker] NVARCHAR(10) NULL, -- 로그인 메이커 정보
+ [Model] NVARCHAR(10) NULL, -- 로그인 모델 정보
+ [Variant_1] NVARCHAR(10) NULL, -- 로그인 변형1 정보
+ [Variant_2] NVARCHAR(10) NULL, -- 로그인 변형2 정보
+ [Operator] NVARCHAR(10) NOT NULL, -- 로그인 검사자 ID
+ [Production_Date] DATETIME2(0) NOT NULL DEFAULT GETDATE(), -- 검사 일시
+ [Line] NVARCHAR(5) NULL, -- 로그인 작업 라인
+ [Lot_No] NVARCHAR(8) NULL, -- 로그인 Lot 번호
+ [Jig_No] NVARCHAR(10) NULL, -- 로그인 지그 번호
+ [Channel] NVARCHAR(5) NOT NULL, -- 검증 수행 채널 (LEFT / RIGHT)
+ [Spec_UL] DECIMAL(3,2) NOT NULL, -- 스펙 상한값
+ [Spec_LL] DECIMAL(3,2) NOT NULL, -- 스펙 하한값
+ [Leak_Value] DECIMAL(5,4) NOT NULL, -- 측정된 누설량
+ [Result] NVARCHAR(5) NOT NULL, -- 합격 여부 판정 (OK / NG)
- CONSTRAINT [PK_EOL_ID] PRIMARY KEY CLUSTERED ([ID] ASC)
+ CONSTRAINT [PK_EOL_IC_SN] PRIMARY KEY CLUSTERED ([IC_SN] ASC),
+ CONSTRAINT [UQ_EOL_PCB_Barcode] UNIQUE ([PCB_Barcode] ASC)
);
GO
-PRINT 'Table [dbo].[EOL] created successfully with simplified columns and ID as Primary Key.';
+PRINT 'Table [dbo].[EOL] created successfully with latest 16 columns and IC_SN as Primary Key.';
GO
-- ============================================================================
--- [부록] 테스트 시나리오 SQL 예시 (단순화 버전)
+-- [부록] 테스트 시나리오 SQL 예시 (신규 개편 버전)
-- ============================================================================
/*
-- 1. 테스트용 기본 데이터 INSERT (정상 삽입)
INSERT INTO [dbo].[EOL] (
- [InspectDate], [InspectTime], [ID], [Channel], [SpecUL], [SpecLL], [LeakValue], [Result]
+ [IC_SN], [PCB_Barcode], [Maker], [Model], [Variant_1], [Variant_2],
+ [Operator], [Production_Date], [Line], [Lot_No], [Jig_No], [Channel],
+ [Spec_UL], [Spec_LL], [Leak_Value], [Result]
)
VALUES (
- '2026-06-02', '14:48:51', 'IC_SIMPLE_0001', 'LEFT', '1.00', '-1.00', '0.125', 'OK'
+ 'SN00000001', 'SN00000001', 'MAKER_A', 'MODEL_X', 'V1', 'V2',
+ 'OP_01', '2026-06-24 11:00:00', 'L1', 'LOT00001', 'JIG01', 'LEFT',
+ 1.00, -1.00, 0.1250, 'OK'
);
-- 데이터 확인
SELECT * FROM [dbo].[EOL];
--- 2. 동일 ID 재검사 시 MERGE 문을 이용한 UPSERT 테스트 (정상 업데이트 확인)
--- 예상 결과: 기존 'IC_SIMPLE_0001' 레코드의 LeakValue가 '0.098'로 업데이트됨
+-- 2. 동일 IC_SN 재검사 시 MERGE 문을 이용한 UPSERT 테스트 (정상 업데이트 확인)
+-- 예상 결과: 기존 'SN00000001' 레코드의 Leak_Value가 '0.0980'으로 업데이트되고, Production_Date 등이 갱신됨
MERGE INTO [dbo].[EOL] AS Target
-USING (SELECT 'IC_SIMPLE_0001' AS ID) AS Source
-ON (Target.ID = Source.ID)
+USING (SELECT 'SN00000001' AS IC_SN) AS Source
+ON (Target.IC_SN = Source.IC_SN)
WHEN MATCHED THEN
UPDATE SET
+ Target.PCB_Barcode = 'SN00000001',
+ Target.Maker = 'MAKER_A',
+ Target.Model = 'MODEL_X',
+ Target.Variant_1 = 'V1',
+ Target.Variant_2 = 'V2',
+ Target.Operator = 'OP_01',
+ Target.Production_Date = '2026-06-24 11:05:00',
+ Target.Line = 'L1',
+ Target.Lot_No = 'LOT00001',
+ Target.Jig_No = 'JIG01',
Target.Channel = 'LEFT',
- Target.SpecUL = '1.00',
- Target.SpecLL = '-1.00',
- Target.LeakValue = '0.098', -- 업데이트될 새 값
- Target.Result = 'OK',
- Target.InspectDate = '2026-06-02',
- Target.InspectTime = '14:48:00'
+ Target.Spec_UL = 1.00,
+ Target.Spec_LL = -1.00,
+ Target.Leak_Value = 0.0980, -- 업데이트될 새 값
+ Target.Result = 'OK'
WHEN NOT MATCHED THEN
- INSERT ([ID], [Channel], [SpecUL], [SpecLL], [LeakValue], [Result], [InspectDate], [InspectTime])
- VALUES ('IC_SIMPLE_0001', 'LEFT', '1.00', '-1.00', '0.098', 'OK', '2026-06-02', '14:48:00');
+ INSERT (IC_SN, PCB_Barcode, Maker, Model, Variant_1, Variant_2, Operator, Production_Date, Line, Lot_No, Jig_No, Channel, Spec_UL, Spec_LL, Leak_Value, Result)
+ VALUES ('SN00000001', 'SN00000001', 'MAKER_A', 'MODEL_X', 'V1', 'V2', 'OP_01', '2026-06-24 11:05:00', 'L1', 'LOT00001', 'JIG01', 'LEFT', 1.00, -1.00, 0.0980, 'OK');
-- 데이터 확인 (값 업데이트 확인)
SELECT * FROM [dbo].[EOL];
diff --git a/leak_test_project/App.xaml.cs b/leak_test_project/App.xaml.cs
index 569508d..addd58c 100644
--- a/leak_test_project/App.xaml.cs
+++ b/leak_test_project/App.xaml.cs
@@ -19,6 +19,44 @@ namespace leak_test_project
{
base.OnStartup(e);
+ // Database.ini 파일이 없는 경우 기본 템플릿 생성
+ try
+ {
+ string dbIniPath = System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory, "Database.ini");
+ if (!System.IO.File.Exists(dbIniPath))
+ {
+ string defaultContent =
+ "; =====================================================================\r\n" +
+ "; Mobi Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)\r\n" +
+ "; =====================================================================\r\n" +
+ "\r\n" +
+ "; [데이터베이스 접속 상세 설정]\r\n" +
+ "; Db_Server : 중앙 MSSQL 데이터베이스 서버의 IP 주소 또는 호스트명\r\n" +
+ "; Db_Name : 검사 데이터를 저장할 대상 데이터베이스 이름\r\n" +
+ "; Db_User : 데이터베이스 접속 계정 ID (예: sa)\r\n" +
+ "; Db_Password: 데이터베이스 접속 계정의 비밀번호\r\n" +
+ "Db_Server=192.168.0.20\r\n" +
+ "Db_Name=NE1aW_PT_SENSOR\r\n" +
+ "Db_User=sa\r\n" +
+ "Db_Password=sa1234\r\n" +
+ "\r\n" +
+ "[Login]\r\n" +
+ "; Procedure: 로그인 검증에 사용할 DB 저장 프로시저명입니다. (공란일 경우 기본 테이블 조회 쿼리 작동)\r\n" +
+ "Procedure=dbo.CheckOperator\r\n" +
+ "\r\n" +
+ "; ProcessName: 로그인 검증 시 전달할 공정 이름입니다.\r\n" +
+ "ProcessName=EOL\r\n" +
+ "\r\n" +
+ "; OfflinePreview: 오프라인 모드 활성화 여부 (true = DB 연결 없이 로그인 통과, false = 실제 DB 인증)\r\n" +
+ "OfflinePreview=false\r\n";
+ System.IO.File.WriteAllText(dbIniPath, defaultContent, System.Text.Encoding.UTF8);
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[App Database.ini Create Error] {ex.Message}");
+ }
+
// 메인 윈도우 생성 및 즉시 표시
var mainWindow = new MainWindow();
this.MainWindow = mainWindow;
diff --git a/leak_test_project/Database.ini b/leak_test_project/Database.ini
new file mode 100644
index 0000000..3491e9c
--- /dev/null
+++ b/leak_test_project/Database.ini
@@ -0,0 +1,24 @@
+; =====================================================================
+; Mobi Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)
+; =====================================================================
+
+; [데이터베이스 접속 상세 설정]
+; Db_Server : 중앙 MSSQL 데이터베이스 서버의 IP 주소 또는 호스트명
+; Db_Name : 검사 데이터를 저장할 대상 데이터베이스 이름
+; Db_User : 데이터베이스 접속 계정 ID (예: sa)
+; Db_Password: 데이터베이스 접속 계정의 비밀번호
+Db_Server=192.168.0.20
+Db_Name=NE1aW_PT_SENSOR
+Db_User=sa
+Db_Password=sa1234
+
+
+[Login]
+; Procedure: 로그인 검증에 사용할 DB 저장 프로시저명입니다. (공란일 경우 기본 테이블 조회 쿼리 작동)
+Procedure=dbo.CheckOperator
+
+; ProcessName: 로그인 검증 시 전달할 공정 이름입니다.
+ProcessName=EOL
+
+; OfflinePreview: 오프라인 모드 활성화 여부 (true = DB 연결 없이 로그인 통과, false = 실제 DB 인증)
+OfflinePreview=false
diff --git a/leak_test_project/Infrastructure/CommunicationBase.cs b/leak_test_project/Infrastructure/CommunicationBase.cs
index ad448e8..500490f 100644
--- a/leak_test_project/Infrastructure/CommunicationBase.cs
+++ b/leak_test_project/Infrastructure/CommunicationBase.cs
@@ -122,7 +122,9 @@ namespace leak_test_project.Infrastructure
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
_serialPort.ReadExisting();
- } catch { }
+ } catch (Exception ex) {
+ System.Diagnostics.Debug.WriteLine($"[CommunicationBase ClearBuffer Error] {ex.Message}");
+ }
}
}
diff --git a/leak_test_project/Manual/.obsidian/workspace.json b/leak_test_project/Manual/.obsidian/workspace.json
index e608e0d..641ea91 100644
--- a/leak_test_project/Manual/.obsidian/workspace.json
+++ b/leak_test_project/Manual/.obsidian/workspace.json
@@ -13,12 +13,12 @@
"state": {
"type": "markdown",
"state": {
- "file": "사용자_메뉴얼.md",
- "mode": "preview",
+ "file": "07_시험_프로세스.md",
+ "mode": "source",
"source": false
},
"icon": "lucide-file",
- "title": "사용자_메뉴얼"
+ "title": "07_시험_프로세스"
}
}
]
@@ -185,11 +185,25 @@
},
"active": "7fc461cd97381c41",
"lastOpenFiles": [
- "10_오류_대응.md",
- "09_로그_관리.md",
+ "06_데이터_조회.md",
+ "Pasted image 20260624104558.png",
+ "05_시험_설정.md",
+ "Pasted image 20260624104531.png",
+ "04_통신_설정.md",
+ "Pasted image 20260624104506.png",
"03_메인_화면.md",
+ "Pasted image 20260624104445.png",
+ "Pasted image 20260624104353.png",
+ "Pasted image 20260624104256.png",
+ "Pasted image 20260624104208.png",
+ "Pasted image 20260624104139.png",
"02_초기_설정.md",
"01_시스템_개요.md",
- "사용자_메뉴얼.md"
+ "08_로그_관리.md",
+ "09_오류_대응.md",
+ "사용자_메뉴얼.md",
+ "07_시험_프로세스.md",
+ "10_오류_대응.md",
+ "09_로그_관리.md"
]
}
\ No newline at end of file
diff --git a/leak_test_project/Manual/01_시스템_개요.md b/leak_test_project/Manual/01_시스템_개요.md
index 21afbfc..65306e9 100644
--- a/leak_test_project/Manual/01_시스템_개요.md
+++ b/leak_test_project/Manual/01_시스템_개요.md
@@ -17,7 +17,6 @@
| **Sentinel C28 연동** | RS-232 시리얼 통신으로 누설 센서의 실시간 측정값을 수신합니다 |
| **4251 보드 연동** | 제품 ID(ZMDI 센서) 판독을 위한 시리얼 통신을 수행합니다 |
| **PCI-7432 DIO 보드** | 디지털 입출력 제어 (시작 신호 수신, OK/NG 출력) |
-| **실시간 I/O 모니터링** | 디지털 입출력 상태를 500ms 주기로 실시간 감시합니다 |
| **검사 기록 관리** | 날짜/판정/ID별 필터링 검색 및 CSV 파일 내보내기를 지원합니다 |
| **SPEC 교차 검증** | 프로그램 판정과 센서 자체 판정을 비교하여 불일치 시 경고합니다 |
| **순차 실행 보장** | 세마포어 잠금으로 좌/우 채널 동시 시험을 방지합니다 |
@@ -98,23 +97,23 @@
```
┌────────────────────────────────────────────────┐
-│ 상단 타이틀 바 (프로그램명, 시간, 창 제어 버튼) │
+│ 상단 타이틀 바 (프로그램명, 시간, 배지, 창 제어)│
├────────────────────────────────────────────────┤
│ │
│ 메인 콘텐츠 영역 │
-│ (Home / I/O Monitor / Data View 전환) │
+│ (Home / Data View 전환) │
│ │
├────────────────────────────────────────────────┤
-│ 하단 메뉴 바 (I/O Monitor, Data, Parameters, │
-│ Comm. Settings, ID Test L/R) │
+│ 하단 메뉴 바 (Data, Parameters, Comm.Settings,│
+│ ID Test L/R) │
└────────────────────────────────────────────────┘
```
-| 영역 | 높이 | 설명 |
-|------|------|------|
-| 상단 타이틀 바 | 55px | 프로그램 제목, 현재 시간, 버전 정보, 창 제어 버튼 |
-| 메인 콘텐츠 | 가변 | 선택된 화면(Home/I-O/Data)이 표시되는 영역 |
-| 하단 메뉴 바 | 75px | 화면 전환 및 설정 팝업 버튼 |
+| 영역 | 설명 |
+|------|------|
+| 상단 타이틀 바 | 프로그램 제목, 실시간 날짜/시간, 현재 로그인된 작업자/모델/라인 정보 배지, 세션 로그아웃 버튼, 시스템 창 제어 버튼 배치 |
+| 메인 콘텐츠 영역 | 선택된 기능 화면(Home 검사 모니터 또는 Data 데이터 조회)이 표시되는 주 콘텐츠 영역 |
+| 하단 메뉴 바 | 각 화면 전환 및 설정을 위한 조작 버튼 영역 |
---
diff --git a/leak_test_project/Manual/02_초기_설정.md b/leak_test_project/Manual/02_초기_설정.md
index 8a58ffe..dbe0b8a 100644
--- a/leak_test_project/Manual/02_초기_설정.md
+++ b/leak_test_project/Manual/02_초기_설정.md
@@ -53,7 +53,50 @@
---
-## 2.3 최초 설정 순서 (Step-by-Step)
+## 2.3 프로그램 로그인 및 오프라인 모드 설정
+
+프로그램 실행이 완료되면 메인 화면이 활성화되기 전, **작업 정보 입력 및 로그인 창**이 모달로 표시됩니다. 로그인을 성공적으로 마쳐야 메인 화면을 조작할 수 있습니다.
+
+### 로그인 필드 구성
+
+| 입력 필드 | 예시 | 설명 |
+|-----------|------|------|
+| **Maker** | `MOBIS` | 제조사 정보 |
+| **Model** | `C28_TEST` | 검사 모델명 |
+| **Variant 1** | `V1` | 세부 파생 모델 정보 1 |
+| **Variant 2** | `V2` | 세부 파생 모델 정보 2 |
+| **ID (Operator)** | `sa` | 작업자 ID |
+| **PW (Password)** | `****` | 비밀번호 (DB 조회용) |
+| **Line** | `EOL_01` | 생산 라인 번호 |
+| **Lot No** | `LOT001` | 로트 번호 |
+| **Jig No** | `JIG_L` | 지그 번호 |
+
+> **참고 (자동완성)**: 각 필드에 마우스를 클릭하거나 포커스를 두면, 이전 로그인 이력이 팝업 메뉴 형식으로 나열되어 간편하게 자동 완성을 사용할 수 있습니다.
+
+### 로그인 방식 설정 (Database.ini)
+
+프로그램 실행 폴더에 위치한 `Database.ini` 파일 설정에 따라 로그인 인증 방식이 달라집니다. (해당 파일이 누락된 경우 프로그램 실행 시 기본 템플릿 정보와 함께 자동으로 생성됩니다.)
+
+```ini
+; [데이터베이스 접속 상세 설정]
+Db_Server=192.168.0.20
+Db_Name=NE1aW_PT_SENSOR
+Db_User=sa
+Db_Password=sa1234
+
+[Login]
+Procedure=dbo.CheckOperator
+ProcessName=EOL
+OfflinePreview=false
+```
+
+- **온라인 인증 모드 (`OfflinePreview=false`)**: 지정된 개별 데이터베이스 접속 정보(`Db_Server`, `Db_Name`, `Db_User`, `Db_Password`)를 기반으로 데이터베이스에 접속하여 Stored Procedure(`Procedure`)로 ID/PW 존재 여부를 직접 확인합니다. DB 서버 연결 실패나 미등록 사용자일 경우 로그인이 거부됩니다.
+- **오프라인 테스트 모드 (`OfflinePreview=true`)**: 데이터베이스를 거치지 않고, 필수 입력 값을 채우면 즉시 로그인이 허용됩니다. 네트워크 연결이 안 된 시뮬레이션 및 테스트 환경에서 사용합니다.
+
+
+---
+
+## 2.4 최초 설정 순서 (Step-by-Step)
프로그램을 처음 사용할 때는 반드시 아래 순서로 설정을 진행하세요.
@@ -122,7 +165,7 @@
---
-## 2.4 설정 변경 시 주의사항
+## 2.5 설정 변경 시 주의사항
| 상황 | 동작 | 주의 |
|------|------|------|
diff --git a/leak_test_project/Manual/03_메인_화면.md b/leak_test_project/Manual/03_메인_화면.md
index 06afc2b..4dd5b61 100644
--- a/leak_test_project/Manual/03_메인_화면.md
+++ b/leak_test_project/Manual/03_메인_화면.md
@@ -7,14 +7,15 @@
프로그램 실행 시 기본으로 표시되는 화면입니다. 좌/우 채널의 누설 시험 결과를 실시간으로 모니터링합니다.
## 3.1 전체 화면 레이아웃
+![[Pasted image 20260624104208.png]]
```
┌────────────────────────────────────────────────────────────────┐
-│ Pressure Leak Inspect System 2026-05-08 [목] 10:30:00 ─□X │ ← 상단 타이틀 바
+│ Pressure Leak v1.0.0 2026-06-23 [화] 10:30:00 [작업자][모델][라인] [로그아웃] ─□X│ ← 타이틀 바
├─────────────────────────────┬──────────────────────────────────┤
│ LEFT LEAK TESTER │ RIGHT LEAK TESTER │
├─────────────────────────────┼──────────────────────────────────┤
-│ 시작 시간 │ 2026-05-08 ... │ 시작 시간 │ 2026-05-08 ... │
+│ 시작 시간 │ 2026-06-23 ... │ 시작 시간 │ 2026-06-23 ... │
│ ID/LOW ID │ ABC123 / 00FF │ ID/LOW ID │ DEF456 / 01AA │
│ 측정값 │ 0.5234 sccm │ 측정값 │ 0.3124 sccm │
│ │ SPEC UL: 1.00 │ │ SPEC UL: 1.00 │
@@ -31,14 +32,16 @@
## 3.2 상단 타이틀 바
-프로그램 최상단에 위치한 짙은 남색(#2C3E50) 배경의 바입니다.
+프로그램 최상단에 위치한 짙은 블랙 그레이 배경의 바입니다.
+![[Pasted image 20260624104256.png]]
| 위치 | 요소 | 설명 |
|------|------|------|
-| 좌측 | **프로그램 제목** | "Pressure Leak Inspect System" (백색, 24pt 볼드) |
-| 중앙~우측 | **현재 날짜/시간** | `2026-05-08 [목] 10:30:00` 형식, 1초 주기로 실시간 갱신 |
-| 우측 | **버전 정보** | `Ver. 1.0.0 | Updated 2026-04-03` (회색 텍스트) |
-| 최우측 | **창 제어 버튼** | 최소화(─) / 최대화·복원(□) / 종료(X, 빨간색) |
+| 좌측 | **프로그램 제목 & 버전** | "Pressure Leak" (백색 볼드) 및 `v1.0.0 (Updated: 2026-06-05)` 표시 |
+| 중앙 | **현재 날짜/시간** | `yyyy-MM-dd [요일] HH:mm:ss` 형식, 1초 주기로 실시간 갱신 |
+| 우측 | **작업 정보 배지** | 현재 로그인 상태에 따른 `작업자 ID`, `검사 모델`, `생산 라인` 정보를 배지로 시각화하여 현재 작업 상태 인지 |
+| 우측 | **로그아웃 버튼** | 현재 세션을 로그아웃하고, 다시 로그인 모달 창을 띄웁니다 |
+| 최우측 | **창 제어 버튼** | 최소화(─) / 최대화·복원(□) / 종료(X)를 수행하는 윈도우 창 제어 아이콘 배치 |
### 타이틀 바 조작
@@ -55,13 +58,13 @@
## 3.3 메인 영역 - 채널 모니터링
화면은 좌측(`LEFT LEAK TESTER`)과 우측(`RIGHT LEAK TESTER`)으로 균등 분할되며, 각 채널은 동일한 구조입니다.
+![[Pasted image 20260624104353.png]]
### 각 채널의 구성 요소 (위에서 아래 순서)
#### ① 채널 타이틀 (Row 0)
- **LEFT LEAK TESTER** 또는 **RIGHT LEAK TESTER**
-- 짙은 남색(#34495E) 배경에 백색 볼드 텍스트 (20pt)
-- 높이: 50px
+- 짙은 남색 배경에 백색 볼드 텍스트로 채널 구분을 명시합니다.
#### ② 시작 시간 (Row 1)
| 항목 | 설명 |
@@ -69,7 +72,6 @@
| 라벨 | `시작 시간` (회색 헤더 배경) |
| 값 | `yyyy-MM-dd HH:mm:ss` 형식 (예: `2026-05-08 10:30:15`) |
| 갱신 시점 | 시험이 시작될 때마다 현재 시각으로 자동 갱신 |
-| 높이 | 42px |
#### ③ ID / LOW ID (Row 2)
| 항목 | 설명 |
@@ -78,7 +80,6 @@
| 값 | `{제품ID} / {Low ID}` 형식으로 조합 표시 |
| 데이터 소스 | 4251 보드를 통해 ZMDI 센서에서 읽은 값 |
| 갱신 시점 | 시험 시작 시 초기화, 센서 판독 완료 시 표시 |
-| 높이 | 42px |
#### ④ 측정값 + SPEC (Row 3) - **가장 큰 영역**
좌측에 측정값, 우측에 SPEC 규격이 함께 표시됩니다.
@@ -87,29 +88,29 @@
| 항목 | 설명 |
|------|------|
-| 라벨 | `측정값` (18pt 볼드) |
+| 라벨 | `측정값` (볼드) |
| 값 | Sentinel C28에서 수신한 누설량 (소수점 4자리, 예: `0.5234`) |
| 단위 | `sccm` (우측 하단에 표시) |
-| 폰트 크기 | **84pt** (매우 크게 표시하여 원거리에서도 확인 가능) |
-| 색상 | 파란색(#2980B9) 볼드 |
+| 폰트 크기 | 원거리에서도 쉽게 식별할 수 있도록 대형 폰트로 강조 표시 |
+| 색상 | 파란색 볼드 |
| 갱신 시점 | C28 센서 스트리밍 데이터 수신 시 실시간 갱신 |
-**SPEC 영역 (우측 210px):**
+**SPEC 영역 (우측):**
| 항목 | 설명 |
|------|------|
-| `UL` | 상한 규격값 (예: `1.00`), 26pt 볼드 |
-| `LL` | 하한 규격값 (예: `-1.00`), 26pt 볼드 |
+| `UL` | 상한 규격값 (예: `1.00`) 볼드 |
+| `LL` | 하한 규격값 (예: `-1.00`) 볼드 |
| 갱신 시점 | Parameters에서 저장 시 즉시 반영 |
#### ⑤ 판정 (Row 4) - **두 번째로 큰 영역**
| 항목 | 설명 |
|------|------|
-| 라벨 | `판정` (18pt 볼드) |
-| 값 | **OK** 또는 **NG** (96pt, ExtraBold) |
-| OK 표시 | 녹색 텍스트(#27AE60) + 연녹색 배경(#D4EFDF) |
-| NG 표시 | 빨간색 텍스트(#C0392B) + 연빨간색 배경(#FADBD8) |
+| 라벨 | `판정` (볼드) |
+| 값 | **OK** 또는 **NG** (대형 볼드) |
+| OK 표시 | 녹색 텍스트 + 연녹색 배경으로 가시성 극대화 |
+| NG 표시 | 빨간색 텍스트 + 연빨간색 배경으로 가시성 극대화 |
> **판정 기준**: `SPEC LL ≤ 측정값 ≤ SPEC UL` → **OK**, 그 외 → **NG**
@@ -118,8 +119,7 @@
| 항목 | 설명 |
|------|------|
| 라벨 | `진행 메시지` (연파랑 배경) |
-| 값 | 현재 시험 단계를 나타내는 텍스트 (22pt 볼드, 파란색) |
-| 높이 | 60px |
+| 값 | 현재 시험 단계를 나타내는 파란색 볼드 텍스트 |
표시되는 메시지 목록:
@@ -140,7 +140,7 @@
| 항목 | 설명 |
|------|------|
| 라벨 | `오류 메시지` (연주황 배경, 갈색 텍스트) |
-| 값 | 오류 내용 (빨간색, 18pt 볼드). 스크롤 가능 |
+| 값 | 오류 내용 (빨간색 볼드). 스크롤 가능 |
| 특징 | 읽기 전용 텍스트 박스, 텍스트 줄바꿈 자동, 세로 스크롤바 |
| 초기화 시점 | 새 시험 시작 시 자동 초기화 |
@@ -159,9 +159,10 @@
## 3.5 하단 메뉴 바
-프로그램 최하단의 연회색(#D5DBDB) 배경 영역입니다. 5개의 버튼이 좌측부터 수평으로 배치됩니다.
+프로그램 최하단의 연회색 배경 영역입니다. 5개의 버튼이 좌측부터 수평으로 배치됩니다.
+![[Pasted image 20260624104445.png]]
-| 순서 | 버튼 텍스트 | 부제 | 동작 | 동작 방식 |
+| 순서 | BUTTON | 부제 | 동작 | 동작 방식 |
|------|------------|------|------|----------|
| 1 | **Data** | (측정 데이터) | 검사 기록 조회 화면 전환 | 토글 (다시 클릭 시 Home으로 복귀) |
| 2 | **Parameters** | (시험 설정) | 시험 설정 팝업 창 열기 | 모달 팝업 (설정 후 자동 닫힘) |
@@ -170,10 +171,9 @@
| 5 | **ID Test (R)** | (우측 ID 테스트) | 우측 채널 센서 ID 수동 판독 | 즉시 실행 (파란색 텍스트) |
### 버튼 스타일
-- 크기: 140px × 55px
-- 배경: 연회색(#F2F4F4), 마우스 오버 시 약간 어두워짐(#E5E8E8)
-- 테두리: 회색(#85929E), 1.5px
-- 텍스트: 14pt 볼드 (부제는 10pt 일반)
+- 배경: 연회색, 마우스 오버 시 시각적 피드백을 위해 약간 어둡게 변함
+- 테두리: 회색
+- 텍스트: 볼드 (부제는 일반 텍스트)
---
diff --git a/leak_test_project/Manual/04_통신_설정.md b/leak_test_project/Manual/04_통신_설정.md
index b56ddb2..6106f19 100644
--- a/leak_test_project/Manual/04_통신_설정.md
+++ b/leak_test_project/Manual/04_통신_설정.md
@@ -5,7 +5,7 @@
---
하단 메뉴에서 **Comm. Settings (통신 설정)** 버튼 클릭 시 열리는 **모달 팝업 창**입니다.
-
+![[Pasted image 20260624104506.png]]
## 4.1 창 구성
```
@@ -24,9 +24,8 @@
| 속성 | 값 |
|------|-----|
-| 창 크기 | 750px × 자동 높이 (최대 700px) |
-| 크기 변경 | 불가 |
-| 위치 | 메인 창 중앙 |
+| 창 크기 | 고정 (크기 변경 불가) |
+| 위치 | 메인 창 중앙에 정렬되어 표시 |
| 타입 | 모달 (이 창이 열려있으면 메인 창 조작 불가) |
## 4.2 4251 보드 설정 (좌측 영역)
@@ -45,10 +44,10 @@
## 4.4 버튼 설명
-| 버튼 | 색상 | 동작 |
+| 버튼 | 특징 | 동작 |
|------|------|------|
-| **설정 저장** | 연녹색 배경, 녹색 테두리 | config.xml에 저장 → 기존 연결 해제 → 새 설정으로 재연결 → "통신 설정이 저장되었습니다." 메시지 → 창 닫힘 |
-| **닫기** | 기본 회색 | 변경 사항 저장하지 않고 창 닫기 |
+| **설정 저장** | 녹색 계열 | config.xml에 저장 → 기존 연결 해제 → 새 설정으로 재연결 → "통신 설정이 저장되었습니다." 메시지 → 창 닫힘 |
+| **닫기** | 회색 계열 | 변경 사항 저장하지 않고 창 닫기 |
## 4.5 저장 시 내부 동작 순서
diff --git a/leak_test_project/Manual/05_시험_설정.md b/leak_test_project/Manual/05_시험_설정.md
index 4075c6f..d2b5dfd 100644
--- a/leak_test_project/Manual/05_시험_설정.md
+++ b/leak_test_project/Manual/05_시험_설정.md
@@ -5,7 +5,7 @@
---
하단 메뉴에서 **Parameters (시험 설정)** 버튼 클릭 시 열리는 **모달 팝업 창**입니다.
-
+![[Pasted image 20260624104531.png]]
## 5.1 창 구성
```
@@ -25,9 +25,8 @@
| 속성 | 값 |
|------|-----|
-| 창 크기 | 550px × 자동 높이 (최대 600px) |
-| 크기 변경 | 불가 |
-| 위치 | 메인 창 중앙 |
+| 창 크기 | 고정 (크기 변경 불가) |
+| 위치 | 메인 창 중앙에 정렬되어 표시 |
| 타입 | 모달 |
## 5.2 설정 항목
@@ -67,4 +66,4 @@
---
-[← 이전: 통신 설정](04_통신_설정.md) | [다음: I/O Monitor →](06_IO_모니터.md)
+[← 이전: 통신 설정](04_통신_설정.md) | [다음: 측정 데이터 조회 →](06_데이터_조회.md)
diff --git a/leak_test_project/Manual/06_데이터_조회.md b/leak_test_project/Manual/06_데이터_조회.md
new file mode 100644
index 0000000..f4c3ade
--- /dev/null
+++ b/leak_test_project/Manual/06_데이터_조회.md
@@ -0,0 +1,99 @@
+# 6. 측정 데이터 조회 (Data View)
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+하단 메뉴에서 **Data (측정 데이터)** 버튼을 클릭하면 표시됩니다. 다시 클릭하면 메인 화면으로 복귀합니다.
+![[Pasted image 20260624104558.png]]
+## 6.1 화면 구성
+
+```
+┌───────────────────────────────────────────────────────────────────────────────────────────┐
+│ INSPECT DATA LIST (측정 기록 조회) │ ← 타이틀 바
+├───────────────────────────────────────────────────────────────────────────────────────────┤
+│ [조회기간] 2026-05-01 ~ 2026-05-08 [판정 결과] 전체 │
+│ [ID 검색] [작업자] [Lot No] [모델] [Search] [CSV] │ ← 2행 필터 영역
+├──────┬──────┬───────┬────────┬──────┬─────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┼
+│검사일│검사시│ IC SN │ PCB │메이커│모델 │변형1 │변형2 │작업자│라인 │Lot No│지그 │... │ ← 데이터 그리드
+├──────┼──────┼───────┼────────┼──────┼─────┼──────┼──────┼──────┼──────┼──────┼──────┼──────┼
+│ ... │ ... │ABC123 │ ABC123 │ MOBI │ EOL │ A │ B │ admin│ 1 │LOT001│ 1 │... │
+├──────┴──────┴───────┴────────┴──────┴─────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┼
+│ [ Close ] │
+└───────────────────────────────────────────────────────────────────────────────────────────┘
+```
+
+## 6.2 필터 조건 상세
+
+### 1행: 공통 기본 필터
+
+| 항목 | UI 타입 | 기본값 | 설명 |
+|------|---------|--------|------|
+| **조회 기간** | DatePicker (달력) x 2 | 오늘로부터 7일 전 ~ 오늘 | 조회 대상 날짜 범위 설정 (달력 확대 표시 적용) |
+| **판정 결과** | 드롭다운 ComboBox | `[전체]` / `OK` / `NG` | 판정 결과에 따른 필터링 (기본값: 전체) |
+
+### 2행: 상세 검색 필터 (DB 스키마 필드 교차 검색)
+
+| 항목 | UI 타입 | 설명 | 기본값 |
+|------|---------|------|--------|
+| **ID 검색** | TextBox (텍스트 입력) | `IC_SN` 또는 `PCB_Barcode` 컬럼에 대한 부분 일치 (LIKE) 검색 | (빈 값 = 필터 제외) |
+| **작업자** | TextBox (텍스트 입력) | 작업자 ID (`Operator`) 컬럼에 대한 부분 일치 검색 | (빈 값 = 필터 제외) |
+| **Lot No** | TextBox (텍스트 입력) | 랏 번호 (`Lot_No`) 컬럼에 대한 부분 일치 검색 | (빈 값 = 필터 제외) |
+| **모델** | TextBox (텍스트 입력) | 모델명 (`Model`) 컬럼에 대한 부분 일치 검색 | (빈 값 = 필터 제외) |
+
+## 6.3 조회 및 내보내기 버튼
+
+| 버튼 | 색상 | 동작 |
+|------|------|------|
+| **Search** | 파란색 계열 | 설정된 2행의 모든 필터 조건으로 SQL Server EOL DB(혹은 CSV)를 부분일치 검색하여 그리드에 표시합니다. |
+| **CSV** | 녹색 계열 | 검색 결과를 새 CSV 파일(16개 전체 컬럼 사양)로 저장합니다. |
+
+### Search 버튼 동작 상세
+1. DB 연결 활성화 시 SQL Server `dbo.EOL` 테이블에서 Like 조건 쿼리 실행.
+2. DB 연결 실패/주소 미설정 시 로컬 CSV 파일에서 인메모리 부분일치 검색 처리 (구형 8컬럼 파일 하위 호환).
+3. 검색 결과가 없으면 "해당 조건의 데이터가 없습니다." 알림 표시.
+
+### CSV 버튼 동작 상세
+1. 검색 결과가 없으면 "저장할 데이터가 없습니다." 알림 표시.
+2. 파일 저장 대화상자 표시 (기본 파일명: `InspectData_yyyyMMdd_HHmmss.csv`).
+3. 저장 완료 시 "CSV 파일로 저장되었습니다." 알림 표시.
+
+## 6.4 데이터 그리드 컬럼
+
+| 컬럼 헤더명 | 바인딩 프로퍼티 | 정렬 | 설명 |
+|------|------|------|------|
+| **검사일자** | `InspectDate` | 좌측 | 검사 일자 (yyyy-MM-dd) |
+| **검사시간** | `InspectTime` | 좌측 | 검사 시각 (HH:mm:ss) |
+| **IC SN (제품ID)** | `ProductId` (`IcSn`) | 좌측 | 제품 고유 IC SN 시리얼 번호 |
+| **PCB 바코드** | `PcbBarcode` | 좌측 | PCB 바코드 식별자 |
+| **메이커** | `Maker` | 좌측 | 로그인 시 입력된 제조사명 |
+| **모델** | `Model` | 좌측 | 로그인 시 입력된 검사 모델명 |
+| **변형 1** | `Variant1` | 좌측 | 로그인 시 입력된 변형 1 사양 |
+| **변형 2** | `Variant2` | 좌측 | 로그인 시 입력된 변형 2 사양 |
+| **작업자** | `Operator` | 좌측 | 작업자 사용자 ID |
+| **작업라인** | `Line` | 좌측 | 로그인 공정 작업 라인 |
+| **Lot No** | `LotNo` | 좌측 | 생산 Lot 번호 |
+| **지그 번호** | `JigNo` | 좌측 | 사용 지그(Jig) 번호 |
+| **검사채널** | `Channel` | 좌측 | 검사 통신 채널 (`LEFT` / `RIGHT`) |
+| **SPEC UL** | `SpecUL` | 좌측 | 상한 규격값 |
+| **SPEC LL** | `SpecLL` | 좌측 | 하한 규격값 |
+| **릭 측정값** | `MeasuredValue` | **우측** | 측정된 리크 유량값 (**볼드 표시**) |
+| **최종판정** | `Judgment` | **중앙** | `OK`(녹색) 또는 `NG`(빨간색) 볼드 강조 |
+
+### 그리드 스타일
+- 격자선: 수평/수직 연회색 격자선 표시
+- 홀수 행 배경: 흰색, 짝수 행 배경: 연회색 배경으로 행 식별 편의 제공
+- 선택된 행: 연파랑 배경에 파란색 텍스트로 강조
+- 컬럼 헤더 클릭 시 해당 열 기준 오름차순/내림차순 정렬 가능 (컬럼 순서 변경 불가)
+- 최소 50행이 항상 표시됩니다 (데이터가 적어도 격자 형태 유지를 위해 빈 행으로 채워짐)
+
+## 6.5 화면 닫기
+
+| 방법 | 설명 |
+|------|------|
+| 하단 **Close** 버튼 클릭 | 메인 화면으로 복귀 |
+| 하단 메뉴 **Data** 버튼 재클릭 | 토글 동작으로 메인 화면으로 복귀 |
+
+---
+
+[← 이전: 시험 설정](05_시험_설정.md) | [다음: 자동 시험 프로세스 →](07_시험_프로세스.md)
diff --git a/leak_test_project/Manual/07_데이터_조회.md b/leak_test_project/Manual/07_데이터_조회.md
deleted file mode 100644
index 369537f..0000000
--- a/leak_test_project/Manual/07_데이터_조회.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# 6. 측정 데이터 조회 (Data View)
-
-[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
-
----
-
-하단 메뉴에서 **Data (측정 데이터)** 버튼을 클릭하면 표시됩니다. 다시 클릭하면 메인 화면으로 복귀합니다.
-
-## 7.1 화면 구성
-
-```
-┌───────────────────────────────────────────────────────────┐
-│ INSPECT DATA LIST (측정 기록 조회) │ ← 타이틀 바
-├───────────────────────────────────────────────────────────┤
-│ [조회기간] 2026-05-01 ~ 2026-05-08 [판정] 전체 [ID검색] │
-│ [Search] [CSV] │ ← 필터 영역
-├──────┬──────┬───────┬────┬──────┬──────┬──────┬────┬─────┤
-│ 날짜 │재시험│ ID │좌우│UL │LL │측정값│판정│비고 │ ← 데이터 그리드
-│ │ │ │ │ │ │ │ │ │
-│ ... │ N │ABC123 │LEFT│ 1.00 │-1.00 │0.5234│ OK │ │
-│ ... │ N │DEF456 │RIGHT│1.00 │-1.00 │1.2000│ NG │ │
-├──────┴──────┴───────┴────┴──────┴──────┴──────┴────┴─────┤
-│ [ Close ] │
-└───────────────────────────────────────────────────────────┘
-```
-
-## 6.2 필터 조건 상세
-
-### 조회 기간 (폭 320px)
-
-| 항목 | UI 타입 | 기본값 | 설명 |
-|------|---------|--------|------|
-| **시작일** | DatePicker (달력) | 오늘로부터 7일 전 | 조회 시작 날짜 |
-| **종료일** | DatePicker (달력) | 오늘 | 조회 종료 날짜 |
-
-> **참고**: 달력 팝업은 1.25배 확대되어 표시됩니다.
-
-### 판정 결과 (폭 120px)
-
-| 항목 | UI 타입 | 선택 가능 값 | 기본값 |
-|------|---------|-------------|--------|
-| **판정 결과** | 드롭다운 | `[전체]` / `OK` / `NG` | `[전체]` |
-
-### ID 검색 (폭 280px)
-
-| 항목 | UI 타입 | 설명 | 기본값 |
-|------|---------|------|--------|
-| **ID 검색** | 텍스트 입력 | 제품 ID에 포함된 텍스트로 부분 일치 검색 | (빈 값 = 전체) |
-
-## 6.3 조회 및 내보내기 버튼
-
-| 버튼 | 색상 | 크기 | 동작 |
-|------|------|------|------|
-| **Search** | 연파랑 배경, 파랑 테두리 | 110×45px | 설정된 필터 조건으로 Logs 폴더의 CSV 파일을 검색합니다 |
-| **CSV** | 연녹색 배경, 녹색 테두리 | 110×45px | 검색 결과를 새 CSV 파일로 저장합니다 (저장 위치 선택 대화상자) |
-
-### Search 버튼 동작 상세
-1. `[프로그램폴더]\Logs\` 폴더에서 조회 기간에 해당하는 `yyyy-MM-dd.csv` 파일을 탐색
-2. 각 CSV 파일의 데이터를 판정/ID 필터 조건으로 필터링
-3. 결과를 날짜 역순(최신순)으로 정렬하여 데이터 그리드에 표시
-4. 검색 결과가 없으면 "해당 조건의 데이터가 없습니다." 알림 표시
-
-### CSV 버튼 동작 상세
-1. 검색 결과가 없으면 "저장할 데이터가 없습니다." 알림 표시
-2. 파일 저장 대화상자 표시 (기본 파일명: `InspectData_yyyyMMdd_HHmmss.csv`)
-3. 저장 완료 시 "CSV 파일로 저장되었습니다." 알림 표시
-
-## 6.4 데이터 그리드 컬럼
-
-| 컬럼 | 폭 | 정렬 | 설명 |
-|------|-----|------|------|
-| **날짜** | 130px | 좌측 | 검사 일자 (yyyy-MM-dd 형식) |
-| **재시험** | 70px | 좌측 | 재시험 여부 (`Y` 또는 `N`) |
-| **ID** | 250px | 좌측 | 제품 ID (ZMDI 센서에서 읽은 값) |
-| **좌/우** | 70px | 좌측 | 검사 채널 (`LEFT` 또는 `RIGHT`) |
-| **SPEC UL** | 80px | 좌측 | 검사 시점의 상한 규격값 |
-| **SPEC LL** | 80px | 좌측 | 검사 시점의 하한 규격값 |
-| **측정값** | 100px | **우측** | 측정된 결과 sccm (**볼드**) |
-| **판정** | 80px | **중앙** | `OK`(녹색, #27AE60) 또는 `NG`(빨간색, #C0392B), **ExtraBold** |
-| **비고** | 나머지(300px~) | 좌측 | 추가 메모 (현재 미사용) |
-
-### 그리드 스타일
-- 행 높이: 35px
-- 격자선: 수평/수직 모두 연회색(#ECF0F1)
-- 홀수 행 배경: 흰색, 짝수 행 배경: 연회색(#F9FBFC)
-- 선택된 행: 연파랑 배경(#D6EAF8), 파란색 텍스트(#2E86C1)
-- 컬럼 헤더 정렬 가능, 컬럼 순서 변경 불가
-- 최소 50행이 항상 표시됩니다 (데이터가 적어도 빈 행으로 채워짐)
-
-## 6.5 화면 닫기
-
-| 방법 | 설명 |
-|------|------|
-| 하단 **Close** 버튼 클릭 | 메인 화면으로 복귀 |
-| 하단 메뉴 **Data** 버튼 재클릭 | 토글 동작으로 메인 화면으로 복귀 |
-
----
-
-[← 이전: 시험 설정](05_시험_설정.md) | [다음: 자동 시험 프로세스 →](08_시험_프로세스.md)
diff --git a/leak_test_project/Manual/08_시험_프로세스.md b/leak_test_project/Manual/07_시험_프로세스.md
similarity index 73%
rename from leak_test_project/Manual/08_시험_프로세스.md
rename to leak_test_project/Manual/07_시험_프로세스.md
index b9330f7..2d7c21c 100644
--- a/leak_test_project/Manual/08_시험_프로세스.md
+++ b/leak_test_project/Manual/07_시험_프로세스.md
@@ -30,40 +30,38 @@
│ 2단계: 센서 정보 읽기 │
│ · 4251 보드를 통해 ZMDI 센서에서 제품 ID 판독 │
│ · ID / LOW ID를 메인 화면에 표시 │
- │ · 읽기 실패해도 빈 데이터("-")로 계속 진행 │
+ │ · ID 읽기 실패 시: "ID 읽기 실패" 오류 출력 후 │
+ │ "NG" 판정으로 DB 비동기 저장 및 조기 종료 (Skip) │
+ │ (DB 저장 실패 시 "DB 저장 실패" 문구 표시) │
└──────────────┬───────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
- │ 3단계: 불량 제품 필터링 │
- │ · 센서의 PrevResult 값 확인 │
- │ · PrevResult = "F" → 정상 (다음 단계로 진행) │
- │ · PrevResult ≠ "F" → "불량 제품 투입" 오류 → 시험 중단│
- └──────────────┬───────────────────────────────────────┘
- ▼
- ┌──────────────────────────────────────────────────────┐
- │ 4단계: LEAK 시험 수행 │
+ │ 3단계: LEAK 시험 수행 │
│ · "LEAK 시험중" 메시지 표시 │
│ · Sentinel C28 센서의 최종 결과(Auto Result) 대기 │
│ · 최대 대기 시간: 30초 │
│ · 타임아웃 시: NG 판정 + "Sentinel C28 통신 타임아웃" │
+ │ (DB 저장 실패 시 "DB 저장 실패" 문구 표시) │
└──────────────┬───────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
- │ 5단계: 판정 │
+ │ 4단계: 판정 │
│ · SPEC LL ≤ 측정값 ≤ SPEC UL → OK │
│ · 범위 밖 → NG │
└──────────────┬───────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
- │ 6단계: SPEC 교차 검증 │
+ │ 5단계: SPEC 교차 검증 │
│ · 프로그램 판정과 센서 자체 판정 비교 │
│ · 센서 판정 정규화: OK/A/P → Accept, NG/R/F → Reject │
│ · 불일치 시: SpecMismatch = true → 팝업 경고 │
└──────────────┬───────────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────┐
- │ 7단계: 결과 출력 │
+ │ 6단계: 결과 출력 및 저장 │
│ · CSV 로그 파일에 검사 데이터 기록 │
+ │ · MSSQL 중앙 데이터베이스 비동기 저장 │
+ │ · DB 저장 실패 시: 오류 메시지 칸에 "DB 저장 실패" 표시 │
│ · DIO OK 또는 NG 출력 ON │
│ · 메인 화면에 측정값, 판정 결과 표시 │
│ · "시험 완료" 메시지 표시 │
@@ -89,10 +87,10 @@
Sentinel C28 센서에서 30초 이내에 응답이 없으면:
-1. "Sentinel C28 통신 타임아웃" 오류 메시지 표시
+1. "Sentinel C28 통신 타임아웃" 오류 메시지 표시 (DB 저장 실패 시 "DB 저장 실패"도 오류 메시지에 추가 표시)
2. 해당 채널 DIO `NG` 출력 ON
3. 측정값 `0.0000`, 판정 `NG`, 센서판정 `T/O` (Timeout)로 기록
-4. CSV 로그 파일에도 NG로 기록
+4. CSV 로그 파일 및 MSSQL 데이터베이스에 NG 결과 비동기 기록
## 7.5 C28 센서 결과 채널 라우팅
@@ -114,12 +112,24 @@ Sentinel C28 센서에서 수신한 데이터는 채널 번호(ChannelNo)로 구
3. 500ms 대기 (UI 피드백용)
4. "센서 정보 읽는 중" 메시지 표시
5. 4251 보드를 통해 ZMDI 센서 ID 판독
-6. 성공 시: ID / LOW ID 표시 + 불량 제품 필터링 확인 + "ID 테스트 완료"
+6. 성공 시: ID / LOW ID 표시 + "ID 테스트 완료"
7. 실패 시: ID를 "-"로 표시 + "ID 테스트 실패"
8. 세마포어 잠금 해제
-> **용도**: 설치 후 하드웨어 연결 상태 확인, 디버깅 시 활용합니다. LEAK 시험(4~7단계)은 수행하지 않습니다.
+> **용도**: 설치 후 하드웨어 연결 상태 확인, 디버깅 시 활용합니다. LEAK 시험(3~6단계)은 수행하지 않습니다.
+
+## 7.7 SPEC 교차 검증 및 판정 저장 기준
+
+시험 과정에서 프로그램(UI) 판정과 C28 센서 자체의 판정이 불일치하는 경우, 데이터베이스 및 로그 저장의 기준은 다음과 같습니다.
+
+### 저장 기준 요약
+* **최종 저장 판정 (Result)**: C28 센서 자체의 판정값과 무관하게, 항상 **프로그램(UI) 판정 기준(`programJudgment`)**으로 데이터베이스 및 로그에 저장됩니다.
+* **동작 예시**: UI 상으로 합격(`OK`)이지만 C28 센서 자체적으로는 불합격(`NG`/`R`)인 경우, 최종 DB(`Result` 컬럼) 및 CSV 로그에는 **`OK` (합격)**로 저장됩니다.
+
+### 교차 검증 실패 시의 동작 (Spec Mismatch)
+* 프로그램 판정과 센서 자체 판정이 다르면 `specMismatch` 플래그가 `true`로 설정됩니다.
+* 불일치 발생 시 콘솔 로그에 경고(`[TestProcess] {side} SPEC Mismatch!`)를 출력하고 UI 완료 이벤트 인자로 전달되지만, **DB에 저장되는 최종 판정 결과(`Result`)에는 영향을 미치지 않고 프로그램 판정값을 유지**합니다.
---
-[← 이전: 측정 데이터 조회](07_데이터_조회.md) | [다음: 로그 관리 →](09_로그_관리.md)
+[← 이전: 측정 데이터 조회](06_데이터_조회.md) | [다음: 로그 관리 →](08_로그_관리.md)
diff --git a/leak_test_project/Manual/08_로그_관리.md b/leak_test_project/Manual/08_로그_관리.md
new file mode 100644
index 0000000..f7acf05
--- /dev/null
+++ b/leak_test_project/Manual/08_로그_관리.md
@@ -0,0 +1,219 @@
+# 8. 로그 및 데이터 관리
+
+[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+
+---
+
+## 8.1 검사 데이터 로그 (CSV)
+
+### 기본 정보
+
+| 항목 | 내용 |
+|------|------|
+| **저장 위치** | `[프로그램 폴더]\Logs\yyyy-MM-dd.csv` |
+| **파일 형식** | CSV (UTF-8 인코딩) |
+| **파일 생성** | 날짜별 자동 생성 (해당 날짜 첫 시험 시) |
+| **데이터 추가** | 매 시험 완료 시 자동으로 해당 날짜 파일에 1행 추가 |
+
+### CSV 헤더 및 컬럼
+
+```
+IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,Lot_No,Jig_No,Channel,Spec_UL,Spec_LL,Leak_Value,Result
+```
+
+| 컬럼 | 예시 | 설명 |
+|------|------|------|
+| `IC_SN` | `260508A00123B01C` | 제품 고유 시리얼 번호 (PK/FK) |
+| `PCB_Barcode` | `260508A00123B01C` | PCB 바코드 고유 식별 정보 (UK) |
+| `Maker` | `Hyundai` | 메이커 정보 |
+| `Model` | `NE1aW` | 모델 정보 |
+| `Variant_1` | `PT` | 변형1 정보 |
+| `Variant_2` | `SENSOR` | 변형2 정보 |
+| `Operator` | `op001` | 작업자 ID |
+| `Production_Date` | `2026-05-08 10:30:15` | 검사 수행 일시 |
+| `Line` | `L1` | 작업 라인 번호 |
+| `Lot_No` | `LOT001` | Lot 번호 |
+| `Jig_No` | `JIG01` | 지그 번호 |
+| `Channel` | `LEFT` | 검사 채널 (LEFT 또는 RIGHT) |
+| `Spec_UL` | `1.00` | 규격 상한값 |
+| `Spec_LL` | `-1.00` | 규격 하한값 |
+| `Leak_Value` | `0.5234` | 측정된 릭 측정값 (소수점 4자리) |
+| `Result` | `OK` | 최종 판정 결과 (OK 또는 NG) |
+
+### CSV 예시
+```csv
+IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,Lot_No,Jig_No,Channel,Spec_UL,Spec_LL,Leak_Value,Result
+260508A00123B01C,260508A00123B01C,Hyundai,NE1aW,PT,SENSOR,op001,2026-05-08 10:30:15,L1,LOT001,JIG01,LEFT,1.00,-1.00,0.5234,OK
+260508A00456D02E,260508A00456D02E,Hyundai,NE1aW,PT,SENSOR,op002,2026-05-08 10:30:45,L1,LOT001,JIG01,RIGHT,1.00,-1.00,1.2000,NG
+```
+
+> **참고**: 타임아웃 발생 시에도 측정값 `0.0000`, 판정 `NG`로 로그에 기록됩니다.
+
+> **참고**: 프로그램은 구형 8개 컬럼 포맷(`Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment`)의 로그 파일에 대해서도 컬럼 크기를 기준으로 자동 감지 및 파싱(하위 호환성)을 수행합니다.
+
+> **참고**: CSV 값에 쉼표, 따옴표, 줄바꿈이 포함된 경우 자동으로 이스케이프 처리됩니다.
+
+---
+
+## 8.2 시스템 로그
+
+### 기본 정보
+
+| 항목 | 내용 |
+|------|------|
+| **저장 위치** | `[프로그램 폴더]\Logs\yyyy-MM-dd_system.log` |
+| **파일 형식** | 텍스트 파일 (UTF-8 인코딩) |
+| **내용** | 통신 오류, 시스템 이벤트 등 |
+
+### 로그 형식
+```
+[HH:mm:ss.fff] [TAG] 메시지 내용
+```
+
+### 예시
+```
+[10:30:00.123] [SYSTEM] 프로그램 시작
+[10:30:01.456] [COMM] Sentinel C28 포트 연결 성공 (COM1)
+[10:35:15.789] [ERROR] Sentinel C28 통신 타임아웃
+```
+
+---
+
+## 8.3 설정 파일
+
+### config.xml
+
+| 항목 | 내용 |
+|------|------|
+| **위치** | 프로그램 실행 폴더 |
+| **생성** | 최초 실행 시 기본값으로 자동 생성 |
+| **수정** | Comm. Settings 또는 Parameters에서 저장 시 갱신 |
+
+```xml
+
+ COM1
+ 9600
+ COM3
+ 115200
+ 5000
+ 1.00
+ -1.00
+
+```
+
+| 설정 키 | 기본값 | 설정 위치 | 설명 |
+|---------|--------|----------|------|
+| `SensorPort` | COM1 | Comm. Settings | C28 센서 COM 포트 |
+| `SensorBaudRate` | 9600 | Comm. Settings | C28 센서 통신 속도 |
+| `Board4251Port` | COM3 | Comm. Settings | 4251 보드 COM 포트 |
+| `Board4251BaudRate` | 115200 | Comm. Settings | 4251 보드 통신 속도 |
+| `Board4251Timeout` | 5000 | (코드 내 고정) | 4251 보드 응답 대기 시간(ms) |
+| `SpecUL` | 1.00 | Parameters | 누설 상한 규격값 |
+| `SpecLL` | -1.00 | Parameters | 누설 하한 규격값 |
+
+### Database.ini
+
+| 항목 | 내용 |
+|------|------|
+| **위치** | 프로그램 실행 폴더 |
+| **생성** | 배포 시 기본 포함 및 파일 누락 상태로 기동 시 기본 양식이 자동 생성됨 |
+| **수정** | 메모장 등의 텍스트 에디터를 통해 연결 주소 및 프로시저 정보 직접 수정 |
+
+```ini
+; [데이터베이스 접속 상세 설정]
+Db_Server=192.168.0.20
+Db_Name=NE1aW_PT_SENSOR
+Db_User=sa
+Db_Password=sa1234
+
+[Login]
+Procedure=dbo.CheckOperator
+ProcessName=EOL
+OfflinePreview=false
+```
+
+| 설정 키 | 예시 | 설명 |
+|---------|--------|------|
+| `Db_Server` | 192.168.0.20 | 데이터베이스 서버 IP 주소 또는 호스트명 |
+| `Db_Name` | NE1aW_PT_SENSOR | 대상 데이터베이스(DB) 이름 |
+| `Db_User` | sa | 데이터베이스 접속 계정 ID |
+| `Db_Password` | sa1234 | 데이터베이스 접속 계정 비밀번호 |
+| `Procedure` | dbo.CheckOperator | 로그인 계정 검증용 저장 프로시저명 |
+| `ProcessName` | EOL | 검증 공정명 |
+| `OfflinePreview` | false | true 시 DB 연결 없이 로그인 즉시 성공 (오프라인 모드) |
+
+### DioConfig.ini
+
+| 항목 | 내용 |
+|------|------|
+| **위치** | `[프로그램 폴더]\Settings\DioConfig.ini` |
+| **생성** | 최초 실행 시 자동 생성 |
+| **수정** | 일반적으로 수정 불필요 (하드웨어 변경 시에만) |
+
+```ini
+[DIO]
+CompanyName=ADLink
+BoardCnt=1
+
+[BOARD_1]
+BoardType=PCI_7432
+BoardNumber=0
+
+[BOARD_1_IN]
+0=LEFT_START,1,좌측 시험 시작 신호
+1=RIGHT_START,1,우측 시험 시작 신호
+
+[BOARD_1_OUT]
+0=LEFT_OK,1,좌측 합격 출력
+1=RIGHT_OK,1,우측 합격 출력
+2=PC_ON,1,PC 가동 신호
+3=LEFT_NG,1,좌측 불합격 출력
+4=RIGHT_NG,1,우측 불합격 출력
+```
+
+**INI 포맷 설명**: `비트인덱스=신호명,활성여부,설명`
+
+---
+
+## 8.4 파일 관리 주의사항
+
+| 주의사항 | 설명 |
+|---------|------|
+| Logs 폴더 삭제 금지 | 검사 기록이 영구 삭제됩니다 |
+| CSV 파일 직접 편집 | Excel 등으로 편집 시 인코딩이 변경될 수 있으므로 주의 |
+| config.xml 직접 편집 | 잘못된 값 입력 시 프로그램 오작동 가능. 프로그램 내 설정 화면 사용 권장 |
+| 디스크 용량 | 일일 수백 건 검사 기준, 연간 약 50~100MB 정도의 로그가 생성됩니다 |
+
+---
+
+## 8.5 데이터베이스 데이터 관리 (dbo.EOL)
+
+매 시험 완료 시 검사 결과 및 로그인 정보가 데이터베이스에 실시간으로 저장(MERGE/UPSERT)됩니다.
+
+### 데이터베이스 테이블 및 컬럼 매핑
+
+* **대상 테이블**: `[dbo].[EOL]`
+* **매핑 상세**:
+
+| DB 컬럼명 | 데이터 타입 | 매핑되는 C# 데이터 | 비고 |
+| :--- | :--- | :--- | :--- |
+| **`IC_SN`** | `NVARCHAR(10)` | 제품의 최종 조합 ID (`inspectData.ProductId`) | **PK / FK** |
+| **`PCB_Barcode`** | `NVARCHAR(10)` | 고유 식별용 바코드 정보 (`inspectData.ProductId` 매핑) | **UK** |
+| `Maker` | `NVARCHAR(10)` | 로그인 제조사 정보 (`App.LoginResult.Maker`) | |
+| `Model` | `NVARCHAR(10)` | 로그인 모델 정보 (`App.LoginResult.Model`) | |
+| `Variant_1` | `NVARCHAR(10)` | 로그인 변형1 정보 (`App.LoginResult.Variant1`) | |
+| `Variant_2` | `NVARCHAR(10)` | 로그인 변형2 정보 (`App.LoginResult.Variant2`) | |
+| `Operator` | `NVARCHAR(10)` | 로그인 검사자 ID (`App.LoginResult.Operator`) | |
+| `Production_Date` | `DATETIME2(0)` | 검사 수행 일시 (`DateTime.Now`) | |
+| `Line` | `NVARCHAR(5)` | 로그인 작업 라인 (`App.LoginResult.LineNo`) | |
+| `Lot_No` | `NVARCHAR(8)` | 로그인 Lot 번호 (`App.LoginResult.LotNo`) | |
+| `Jig_No` | `NVARCHAR(10)` | 로그인 지그 번호 (`App.LoginResult.JigNo`) | |
+| `Channel` | `NVARCHAR(5)` | 검사 채널 (`LEFT` / `RIGHT`) | |
+| `Spec_UL` | `DECIMAL(3,2)` | 규격 상한값 (소수점 2자리) | |
+| `Spec_LL` | `DECIMAL(3,2)` | 규격 하한값 (소수점 2자리) | |
+| `Leak_Value` | `DECIMAL(5,4)` | 측정된 릭 값 (소수점 4자리) | |
+| `Result` | `NVARCHAR(5)` | 최종 판정 결과 (`OK` 또는 `NG`) | |
+
+---
+
+[← 이전: 시험 프로세스](07_시험_프로세스.md) | [다음: 오류 대응 →](09_오류_대응.md)
diff --git a/leak_test_project/Manual/09_로그_관리.md b/leak_test_project/Manual/09_로그_관리.md
deleted file mode 100644
index 9b45644..0000000
--- a/leak_test_project/Manual/09_로그_관리.md
+++ /dev/null
@@ -1,150 +0,0 @@
-# 8. 로그 및 데이터 관리
-
-[← 메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
-
----
-
-## 8.1 검사 데이터 로그 (CSV)
-
-### 기본 정보
-
-| 항목 | 내용 |
-|------|------|
-| **저장 위치** | `[프로그램 폴더]\Logs\yyyy-MM-dd.csv` |
-| **파일 형식** | CSV (UTF-8 인코딩) |
-| **파일 생성** | 날짜별 자동 생성 (해당 날짜 첫 시험 시) |
-| **데이터 추가** | 매 시험 완료 시 자동으로 해당 날짜 파일에 1행 추가 |
-
-### CSV 헤더 및 컬럼
-
-```
-Date,Time,Channel,ID,Value,Judgment,SpecUL,SpecLL,Retest
-```
-
-| 컬럼 | 예시 | 설명 |
-|------|------|------|
-| `Date` | `2026-05-08` | 검사 일자 |
-| `Time` | `10:30:15` | 검사 시각 |
-| `Channel` | `LEFT` | 검사 채널 (LEFT 또는 RIGHT) |
-| `ID` | `260508A00123B01C` | 제품 ID |
-| `Value` | `0.5234` | 측정값 (sccm, 소수점 4자리) |
-| `Judgment` | `OK` | 판정 결과 (OK 또는 NG) |
-| `SpecUL` | `1.00` | 검사 시점의 상한 규격값 |
-| `SpecLL` | `-1.00` | 검사 시점의 하한 규격값 |
-| `Retest` | `N` | 재시험 여부 (Y 또는 N) |
-
-### CSV 예시
-```csv
-Date,Time,Channel,ID,Value,Judgment,SpecUL,SpecLL,Retest
-2026-05-08,10:30:15,LEFT,260508A00123B01C,0.5234,OK,1.00,-1.00,N
-2026-05-08,10:30:45,RIGHT,260508A00456D02E,1.2000,NG,1.00,-1.00,N
-2026-05-08,10:31:20,LEFT,260508A00789F03G,0.0000,NG,1.00,-1.00,N
-```
-
-> **참고**: 타임아웃 발생 시에도 측정값 `0.0000`, 판정 `NG`로 로그에 기록됩니다.
-
-> **참고**: CSV 값에 쉼표, 따옴표, 줄바꿈이 포함된 경우 자동으로 이스케이프 처리됩니다.
-
----
-
-## 8.2 시스템 로그
-
-### 기본 정보
-
-| 항목 | 내용 |
-|------|------|
-| **저장 위치** | `[프로그램 폴더]\Logs\yyyy-MM-dd_system.log` |
-| **파일 형식** | 텍스트 파일 (UTF-8 인코딩) |
-| **내용** | 통신 오류, 시스템 이벤트 등 |
-
-### 로그 형식
-```
-[HH:mm:ss.fff] [TAG] 메시지 내용
-```
-
-### 예시
-```
-[10:30:00.123] [SYSTEM] 프로그램 시작
-[10:30:01.456] [COMM] Sentinel C28 포트 연결 성공 (COM1)
-[10:35:15.789] [ERROR] Sentinel C28 통신 타임아웃
-```
-
----
-
-## 8.3 설정 파일
-
-### config.xml
-
-| 항목 | 내용 |
-|------|------|
-| **위치** | 프로그램 실행 폴더 |
-| **생성** | 최초 실행 시 기본값으로 자동 생성 |
-| **수정** | Comm. Settings 또는 Parameters에서 저장 시 갱신 |
-
-```xml
-
- COM1
- 9600
- COM3
- 115200
- 5000
- 1.00
- -1.00
-
-```
-
-| 설정 키 | 기본값 | 설정 위치 | 설명 |
-|---------|--------|----------|------|
-| `SensorPort` | COM1 | Comm. Settings | C28 센서 COM 포트 |
-| `SensorBaudRate` | 9600 | Comm. Settings | C28 센서 통신 속도 |
-| `Board4251Port` | COM3 | Comm. Settings | 4251 보드 COM 포트 |
-| `Board4251BaudRate` | 115200 | Comm. Settings | 4251 보드 통신 속도 |
-| `Board4251Timeout` | 5000 | (코드 내 고정) | 4251 보드 응답 대기 시간(ms) |
-| `SpecUL` | 1.00 | Parameters | 누설 상한 규격값 |
-| `SpecLL` | -1.00 | Parameters | 누설 하한 규격값 |
-
-### DioConfig.ini
-
-| 항목 | 내용 |
-|------|------|
-| **위치** | `[프로그램 폴더]\Settings\DioConfig.ini` |
-| **생성** | 최초 실행 시 자동 생성 |
-| **수정** | 일반적으로 수정 불필요 (하드웨어 변경 시에만) |
-
-```ini
-[DIO]
-CompanyName=ADLink
-BoardCnt=1
-
-[BOARD_1]
-BoardType=PCI_7432
-BoardNumber=0
-
-[BOARD_1_IN]
-0=LEFT_START,1,좌측 시험 시작 신호
-1=RIGHT_START,1,우측 시험 시작 신호
-
-[BOARD_1_OUT]
-0=LEFT_OK,1,좌측 합격 출력
-1=RIGHT_OK,1,우측 합격 출력
-2=PC_ON,1,PC 가동 신호
-3=LEFT_NG,1,좌측 불합격 출력
-4=RIGHT_NG,1,우측 불합격 출력
-```
-
-**INI 포맷 설명**: `비트인덱스=신호명,활성여부,설명`
-
----
-
-## 8.4 파일 관리 주의사항
-
-| 주의사항 | 설명 |
-|---------|------|
-| Logs 폴더 삭제 금지 | 검사 기록이 영구 삭제됩니다 |
-| CSV 파일 직접 편집 | Excel 등으로 편집 시 인코딩이 변경될 수 있으므로 주의 |
-| config.xml 직접 편집 | 잘못된 값 입력 시 프로그램 오작동 가능. 프로그램 내 설정 화면 사용 권장 |
-| 디스크 용량 | 일일 수백 건 검사 기준, 연간 약 50~100MB 정도의 로그가 생성됩니다 |
-
----
-
-[← 이전: 시험 프로세스](08_시험_프로세스.md) | [다음: 오류 대응 →](10_오류_대응.md)
diff --git a/leak_test_project/Manual/10_오류_대응.md b/leak_test_project/Manual/09_오류_대응.md
similarity index 79%
rename from leak_test_project/Manual/10_오류_대응.md
rename to leak_test_project/Manual/09_오류_대응.md
index fc67767..c507c26 100644
--- a/leak_test_project/Manual/10_오류_대응.md
+++ b/leak_test_project/Manual/09_오류_대응.md
@@ -15,8 +15,6 @@
> **참고**: 통신 설정을 저장하면 기존 연결을 모두 해제하고 새 설정으로 재연결을 시도합니다. 포트 변경 후 재시작 없이 바로 적용됩니다.
----
-
## 9.2 시험 중 오류
시험 프로세스 진행 중 발생하는 오류입니다. 메인 화면의 **오류 메시지** 영역에 표시됩니다.
@@ -24,11 +22,16 @@
| 오류 메시지 | 원인 | 조치 방법 |
|------------|------|----------|
| `Sentinel C28 통신 타임아웃` | 30초 내 C28 센서 응답 없음 | ① 센서 전원 ON 확인 ② 시리얼 케이블 연결 확인 ③ BaudRate 설정 확인 (기본: 9600) |
-| `불량 제품 투입 (이전 결과: X)` | ZMDI 센서의 PrevResult가 "F"가 아님 (이전 검사 불합격 이력) | 해당 제품을 라인에서 제거하고 정상 제품 투입 |
+| `통신 실패 (4251 보드 CHx 상태 타임아웃)` | 4251 보드가 응답 시간(15초) 내에 상태 응답을 회신하지 않음 | ① 4251 보드 전원 공급 및 케이블 연결선 확인 ② 통신 노이즈 방지 조치 확인 |
+| `통신 실패 (4251 보드 CHx 상태 이상 또는 Fail)` | 4251 보드로부터 정상 응답을 받지 못했거나 오류 코드가 반환됨 | ① 하드웨어 오결선 여부 점검 ② 채널 센서 구동 전원 확인 |
+| `통신 실패 (4251 보드 CHx ID 대기 타임아웃)` | 4251 보드를 통해 ZMDI 센서 ID를 15초 내에 판독하지 못함 | ① 센서가 지그(Jig)에 올바르게 안착되었는지 물리적 확인 ② 접촉 단자 이물질 청소 |
| `SPEC 불일치 - 프로그램: OK, 센서: R` | 프로그램 SPEC과 센서 자체 SPEC의 판정이 다름 | ① 프로그램의 SPEC UL/LL 확인 (Parameters) ② C28 센서의 SPEC 설정 확인 ③ 양쪽 설정값 동기화 |
| `LEAK 센서 데이터 오류` | C28에서 수신된 데이터 파싱 실패 | ① 센서 통신 상태 확인 ② 케이블 접촉 불량 확인 ③ BaudRate 불일치 확인 |
+| `DB 저장 실패` | 중앙 MSSQL DB 서버에 검사 데이터를 저장하지 못함 | ① PC 네트워크 케이블 및 통신 연결 확인 ② Database.ini 파일의 접속 정보(IP/비밀번호 등) 확인 ③ 로컬 일일 CSV 로그 파일(`Logs/yyyy-MM-dd.csv`)에 백업이 잘 저장되었는지 확인 |
+
+---
-### SPEC 교차 검증 경고 팝업
+## 9.3 SPEC 교차 검증 경고 팝업
프로그램 판정과 센서 판정이 불일치하면 팝업 경고가 표시됩니다:
@@ -42,18 +45,18 @@
---
-## 9.3 DIO 보드 오류
+## 9.4 DIO 보드 오류
| 상황 | 프로그램 동작 | 영향 |
|------|-------------|------|
| PCI-7432 보드 미인식 | **시뮬레이션 모드**로 자동 전환 | DIO 입출력이 동작하지 않음 (실제 입출력 포트 제어 불가) |
| 보드 런타임 오류 | 좌/우 채널 오류 메시지에 `[DIO Board Error]` 표시 | 오류 내용에 따라 조치 |
-> **시뮬레이션 모드**: ADLINK 드라이버가 설치되지 않은 개발 환경에서도 프로그램의 나머지 기능(통신 설정, 데이터 조회 등)을 테스트할 수 있도록 자동 전환됩니다.
+> **시뮬레이션 모드**: ADLINK 드라이버가 신설되지 않은 개발 환경에서도 프로그램의 나머지 기능(통신 설정, 데이터 조회 등)을 테스트할 수 있도록 자동 전환됩니다.
---
-## 9.4 일반적인 문제 해결
+## 9.5 일반적인 문제 해결
| 증상 | 가능한 원인 | 해결 방법 |
|------|-----------|----------|
@@ -66,7 +69,7 @@
---
-## 9.5 프로그램 종료
+## 9.6 프로그램 종료
### 종료 방법
@@ -103,4 +106,4 @@
---
-[← 이전: 로그 관리](09_로그_관리.md) | [메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
+[← 이전: 로그 관리](08_로그_관리.md) | [메뉴얼 목차로 돌아가기](사용자_메뉴얼.md)
diff --git a/leak_test_project/Manual/Pasted image 20260624104139.png b/leak_test_project/Manual/Pasted image 20260624104139.png
new file mode 100644
index 0000000..5cf7716
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104139.png differ
diff --git a/leak_test_project/Manual/Pasted image 20260624104208.png b/leak_test_project/Manual/Pasted image 20260624104208.png
new file mode 100644
index 0000000..d961026
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104208.png differ
diff --git a/leak_test_project/Manual/Pasted image 20260624104256.png b/leak_test_project/Manual/Pasted image 20260624104256.png
new file mode 100644
index 0000000..564bec5
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104256.png differ
diff --git a/leak_test_project/Manual/Pasted image 20260624104353.png b/leak_test_project/Manual/Pasted image 20260624104353.png
new file mode 100644
index 0000000..bd6e10f
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104353.png differ
diff --git a/leak_test_project/Manual/Pasted image 20260624104445.png b/leak_test_project/Manual/Pasted image 20260624104445.png
new file mode 100644
index 0000000..7c2136d
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104445.png differ
diff --git a/leak_test_project/Manual/Pasted image 20260624104506.png b/leak_test_project/Manual/Pasted image 20260624104506.png
new file mode 100644
index 0000000..3ec8702
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104506.png differ
diff --git a/leak_test_project/Manual/Pasted image 20260624104531.png b/leak_test_project/Manual/Pasted image 20260624104531.png
new file mode 100644
index 0000000..c537862
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104531.png differ
diff --git a/leak_test_project/Manual/Pasted image 20260624104558.png b/leak_test_project/Manual/Pasted image 20260624104558.png
new file mode 100644
index 0000000..f61e604
Binary files /dev/null and b/leak_test_project/Manual/Pasted image 20260624104558.png differ
diff --git a/leak_test_project/Manual/사용자_메뉴얼.md b/leak_test_project/Manual/사용자_메뉴얼.md
index 0916b06..c076e19 100644
--- a/leak_test_project/Manual/사용자_메뉴얼.md
+++ b/leak_test_project/Manual/사용자_메뉴얼.md
@@ -11,14 +11,14 @@
| 번호 | 문서 | 설명 |
|------|------|------|
| 1 | [시스템 개요 및 요구사항](01_시스템_개요.md) | 프로그램 소개, 주요 기능, 하드웨어/소프트웨어 요구사항, DIO 신호 매핑 |
-| 2 | [초기 설정 가이드](02_초기_설정.md) | 설치 전 준비, 프로그램 실행, 최초 설정 순서 (통신→시험→I/O 확인) |
-| 3 | [메인 화면 (Home View)](03_메인_화면.md) | 타이틀 바, 좌/우 채널 모니터링 영역, 하단 메뉴 바 상세 설명 |
+| 2 | [초기 설정 가이드](02_초기_설정.md) | 설치 전 준비, 로그인 및 프로그램 실행, 최초 설정 순서 (통신→시험→동작 테스트) |
+| 3 | [메인 화면 (Home View)](03_메인_화면.md) | 타이틀 바(작업자/모델/라인 정보 배지 및 로그아웃), 좌/우 채널 모니터링 영역, 하단 메뉴 바 상세 설명 |
| 4 | [통신 설정 (Comm. Settings)](04_통신_설정.md) | 4251 보드 및 Sentinel C28 센서의 COM 포트/BaudRate 설정 방법 |
| 5 | [시험 설정 (Parameters)](05_시험_설정.md) | SPEC UL/LL 판정 규격값 설정 방법 |
-| 6 | [측정 데이터 조회 (Data View)](07_데이터_조회.md) | 검사 기록 검색, 필터링, CSV 내보내기 방법 |
-| 7 | [자동 시험 프로세스](08_시험_프로세스.md) | 7단계 자동 시험 흐름, 순차 실행, ID 수동 테스트 |
-| 8 | [로그 및 데이터 관리](09_로그_관리.md) | 검사 데이터 로그, 시스템 로그, 설정 파일 구조 |
-| 9 | [오류 대응 및 프로그램 종료](10_오류_대응.md) | 통신 오류, 시험 중 오류, DIO 보드 오류 대응, 종료 절차 |
+| 6 | [측정 데이터 조회 (Data View)](06_데이터_조회.md) | 검사 기록 검색, 필터링, CSV 내보내기 방법 |
+| 7 | [자동 시험 프로세스](07_시험_프로세스.md) | 7단계 자동 시험 흐름, 순차 실행, ID 수동 테스트 |
+| 8 | [로그 및 데이터 관리](08_로그_관리.md) | 검사 데이터 로그, 시스템 로그, 설정 파일 구조 |
+| 9 | [오류 대응 및 프로그램 종료](09_오류_대응.md) | 통신 오류, 시험 중 오류, DIO 보드 오류 대응, 종료 절차 |
---
@@ -27,10 +27,10 @@
처음 사용하는 경우 아래 순서를 따르세요:
1. **하드웨어 연결 확인** → [시스템 개요 및 요구사항](01_시스템_개요.md) 참조
-2. **프로그램 실행** → `leak_test_project.exe` 더블 클릭
+2. **프로그램 실행 및 로그인** → `leak_test_project.exe` 더블 클릭 후 작업 정보 입력 및 로그인 완료 (데모 계정은 ID/PW `test`/`test` 입력, 오프라인 환경일 경우 `Database.ini`에서 `OfflinePreview=true` 설정 확인)
3. **통신 설정** → 하단 메뉴 `Comm. Settings` → COM 포트 선택 → `설정 저장`
4. **시험 설정** → 하단 메뉴 `Parameters` → SPEC UL/LL 입력 → `설정 저장`
-5. **시험 시작** → DIO 시작 신호 또는 `ID Test` 버튼으로 동작 확인
+5. **시험 시작** → DIO 시작 신호 또는 `ID Test (L/R)` 버튼으로 동작 확인
> **참고**: 각 단계의 상세 내용은 위 문서 링크를 참조하세요.
diff --git a/leak_test_project/Models/ProjectModels.cs b/leak_test_project/Models/ProjectModels.cs
index 9760a57..3746696 100644
--- a/leak_test_project/Models/ProjectModels.cs
+++ b/leak_test_project/Models/ProjectModels.cs
@@ -16,7 +16,10 @@ namespace leak_test_project.Models
public double SpecUL { get; set; } = 1.00;
public double SpecLL { get; set; } = -1.00;
- /// MSSQL 데이터베이스 연결 문자열
+ ///
+ /// MSSQL 데이터베이스 연결 문자열 (더 이상 사용되지 않음. 대신 Database.ini의 설정을 읽어 접속합니다.)
+ ///
+ [Obsolete("Use Database.ini ConnectionString instead.")]
public string DbConnectionString { get; set; } = "Server=192.168.0.20;Database=NE1aW_PT_SENSOR;User Id=sa;Password=sa1234;";
}
@@ -91,6 +94,19 @@ namespace leak_test_project.Models
public string SpecLL { get; set; }
public string MeasuredValue { get; set; }
public string Judgment { get; set; }
+
+ // 신규 DB 테이블 EOL 컬럼 매핑 및 정합성 보장을 위한 추가 프로퍼티
+ public string IcSn { get; set; } = string.Empty;
+ public string PcbBarcode { get; set; } = string.Empty;
+ public string Maker { get; set; } = string.Empty;
+ public string Model { get; set; } = string.Empty;
+ public string Variant1 { get; set; } = string.Empty;
+ public string Variant2 { get; set; } = string.Empty;
+ public string Operator { get; set; } = string.Empty;
+ public DateTime? ProductionDate { get; set; }
+ public string Line { get; set; } = string.Empty;
+ public string LotNo { get; set; } = string.Empty;
+ public string JigNo { get; set; } = string.Empty;
}
public class ParsedData
@@ -154,7 +170,6 @@ namespace leak_test_project.Models
/// 최종 조합 ID (년+월+일+시리얼+라인+항목)
public string ID { get; set; } = "";
- /// 이전 검사 결과. F=미시험/불합격, P=합격. 불량 제품 필터링에 사용.
- public string PrevResult { get; set; } = "";
+
}
}
diff --git a/leak_test_project/Services/Board4251.cs b/leak_test_project/Services/Board4251.cs
index 883a573..300052d 100644
--- a/leak_test_project/Services/Board4251.cs
+++ b/leak_test_project/Services/Board4251.cs
@@ -290,7 +290,7 @@ namespace leak_test_project.Services
public bool Connect() => _service.Connect();
public void Disconnect() => _service.Disconnect();
- public SensorIdData ReadSensor()
+ public async Task ReadSensorAsync()
{
try
{
@@ -298,23 +298,33 @@ namespace leak_test_project.Services
// 0. 초기화 (x00o 전송)
ProgressMessage?.Invoke(this, "4251 보드 초기화 중...");
- // x00o 명령을 보내고 응답 내용에 상관없이 다음 단계 진행 (최대 5초 대기)
- Task.Run(() => _service.SendCommandAsync("x00o\r\n")).Wait(1000);
- Task.Delay(350).Wait(); // 보드 안정화 대기
+ // x00o 명령을 보내고 응답 내용에 상관없이 다음 단계 진행
+ _ = _service.SendCommandAsync("x00o\r\n");
+ await Task.Delay(350); // 보드 안정화 대기
// 1. 보드 상태 확인 (Fail인 경우 중단)
ProgressMessage?.Invoke(this, "4251 보드 데이터 읽는 중...");
int extendedTimeout = 15000;
string statusCmd = $"x00c_00{channel}101:owt28006727ea97c7801";
- var statusTask = Task.Run(() => _service.CheckStatusAsync(channel));
- if (!statusTask.Wait(extendedTimeout))
+ bool statusResult = false;
+
+ using (var cts = new CancellationTokenSource(extendedTimeout))
{
- string buf = _service.GetLastBuffer();
- string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
- ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} 상태 타임아웃)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}");
- return null;
+ 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();
+ string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
+ ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} 상태 타임아웃)\r\n[송신값]: {statusCmd}\r\n[수신값]: {displayBuf}");
+ return null;
+ }
+ cts.Cancel();
+ statusResult = await statusTask;
}
- if (!statusTask.Result)
+
+ if (!statusResult)
{
string buf = _service.GetLastBuffer();
string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
@@ -329,20 +339,26 @@ namespace leak_test_project.Services
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
- var idTask = Task.Run(() => _service.ReadIdAsync(channel));
- if (!idTask.Wait(extendedTimeout))
+ using (var cts = new CancellationTokenSource(extendedTimeout))
{
- if (attempt == maxAttempts)
+ var idTask = _service.ReadIdAsync(channel);
+ var delayTask = Task.Delay(extendedTimeout, cts.Token);
+ var completedTask = await Task.WhenAny(idTask, delayTask);
+ if (completedTask == delayTask)
{
- string buf = _service.GetLastBuffer();
- string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
- ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} ID 대기 타임아웃)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}");
- return null;
+ if (attempt == maxAttempts)
+ {
+ string buf = _service.GetLastBuffer();
+ string displayBuf = string.IsNullOrEmpty(buf) ? "수신된 데이터 없음" : buf;
+ ErrorMessage?.Invoke(this, $"통신 실패 (4251 보드 CH{channel} ID 대기 타임아웃)\r\n[송신값]: {idCmd}\r\n[수신값]: {displayBuf}");
+ return null;
+ }
+ continue;
}
- continue;
+ cts.Cancel();
+ rawId = await idTask;
}
- rawId = idTask.Result;
if (string.IsNullOrEmpty(rawId))
{
if (attempt == maxAttempts)
@@ -361,7 +377,7 @@ namespace leak_test_project.Services
if (attempt < maxAttempts)
{
ProgressMessage?.Invoke(this, $"ID 읽기 재시도 중... ({attempt}/{maxAttempts})");
- Task.Delay(350).Wait(); // 재시도 전 약간의 딜레이
+ await Task.Delay(350); // 재시도 전 약간의 딜레이
continue;
}
else
@@ -382,8 +398,7 @@ namespace leak_test_project.Services
LowID = rawId,
ID = rawId, // 16자리 전체를 ID로 사용
Serial = "", // 시리얼 번호는 현재 존재하지 않으므로 강제로 파싱하지 않음
- Item = "N/A",
- PrevResult = "F" // '불량제품 투입' 필터를 통과하기 위한 강제 초기화
+ Item = "N/A"
};
ProgressMessage?.Invoke(this, "ID 읽기 성공");
diff --git a/leak_test_project/Services/DatabaseSettings.cs b/leak_test_project/Services/DatabaseSettings.cs
new file mode 100644
index 0000000..89fce44
--- /dev/null
+++ b/leak_test_project/Services/DatabaseSettings.cs
@@ -0,0 +1,122 @@
+using System;
+using System.IO;
+
+namespace Housing.Services
+{
+ public class DatabaseSettings
+ {
+ 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
+ {
+ // 개별 설정이 모두 지정되어 있는 경우 조합해서 사용
+ if (!string.IsNullOrWhiteSpace(Db_Server) &&
+ !string.IsNullOrWhiteSpace(Db_Name) &&
+ !string.IsNullOrWhiteSpace(Db_User) &&
+ !string.IsNullOrWhiteSpace(Db_Password))
+ {
+ return $"Server={Db_Server};Database={Db_Name};User Id={Db_User};Password={Db_Password};";
+ }
+ // 없으면 레거시 ConnectionString 필드를 그대로 사용
+ return _rawConnectionString;
+ }
+ set
+ {
+ _rawConnectionString = value;
+ }
+ }
+
+ private static readonly object _dbLock = new object();
+ private static DatabaseSettings _cachedSettings;
+ private static DateTime _lastWriteTime;
+ private static string _cachedPath;
+
+ public static DatabaseSettings Load(string path)
+ {
+ lock (_dbLock)
+ {
+ try
+ {
+ if (File.Exists(path))
+ {
+ var currentWriteTime = File.GetLastWriteTime(path);
+ if (_cachedSettings != null && _cachedPath == path && _lastWriteTime == currentWriteTime)
+ {
+ return _cachedSettings;
+ }
+
+ var settings = new DatabaseSettings();
+ var lines = File.ReadAllLines(path);
+ 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)
+ {
+ 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();
+ }
+ }
+ }
+ _cachedSettings = settings;
+ _lastWriteTime = currentWriteTime;
+ _cachedPath = path;
+ return settings;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[DatabaseSettings Load Error] {ex.Message}");
+ }
+ return _cachedSettings ?? new DatabaseSettings();
+ }
+ }
+ }
+}
diff --git a/leak_test_project/Services/EolDatabaseService.cs b/leak_test_project/Services/EolDatabaseService.cs
index 27787ea..3762db6 100644
--- a/leak_test_project/Services/EolDatabaseService.cs
+++ b/leak_test_project/Services/EolDatabaseService.cs
@@ -21,46 +21,84 @@ namespace leak_test_project.Services
/// EOL 검사 결과를 데이터베이스에 저장합니다. (동일 ID 존재 시 업데이트, 미존재 시 삽입)
///
/// 검사 결과 데이터
- public async Task SaveEolDataAsync(InspectData inspectData)
+ /// 저장 성공 여부 (성공: true, 실패: false)
+ public async Task SaveEolDataAsync(InspectData inspectData, Housing.Login.StartupLoginWindowResult loginInfo)
{
- if (inspectData == null) return;
+ if (inspectData == null) return false;
- // MSSQL MERGE 문을 사용한 UPSERT 처리
+ // MSSQL MERGE 문을 사용한 UPSERT 처리 (신규 dbo.EOL 스키마 적용)
const string query = @"
MERGE INTO [dbo].[EOL] AS Target
- USING (SELECT @ID AS ID) AS Source
- ON (Target.ID = Source.ID)
+ USING (SELECT @IC_SN AS IC_SN) AS Source
+ ON (Target.IC_SN = Source.IC_SN)
WHEN MATCHED THEN
UPDATE SET
+ Target.PCB_Barcode = @PCB_Barcode,
+ Target.Maker = @Maker,
+ Target.Model = @Model,
+ Target.Variant_1 = @Variant_1,
+ Target.Variant_2 = @Variant_2,
+ Target.Operator = @Operator,
+ Target.Production_Date = @Production_Date,
+ Target.Line = @Line,
+ Target.Lot_No = @Lot_No,
+ Target.Jig_No = @Jig_No,
Target.Channel = @Channel,
- Target.SpecUL = @SpecUL,
- Target.SpecLL = @SpecLL,
- Target.LeakValue = @LeakValue,
- Target.Result = @Result,
- Target.InspectDate = @InspectDate,
- Target.InspectTime = @InspectTime
+ Target.Spec_UL = @Spec_UL,
+ Target.Spec_LL = @Spec_LL,
+ Target.Leak_Value = @Leak_Value,
+ Target.Result = @Result
WHEN NOT MATCHED THEN
- INSERT ([ID], [Channel], [SpecUL], [SpecLL], [LeakValue], [Result], [InspectDate], [InspectTime])
- VALUES (@ID, @Channel, @SpecUL, @SpecLL, @LeakValue, @Result, @InspectDate, @InspectTime);";
+ INSERT (IC_SN, PCB_Barcode, Maker, Model, Variant_1, Variant_2, Operator, Production_Date, Line, Lot_No, Jig_No, Channel, Spec_UL, Spec_LL, Leak_Value, Result)
+ VALUES (@IC_SN, @PCB_Barcode, @Maker, @Model, @Variant_1, @Variant_2, @Operator, @Production_Date, @Line, @Lot_No, @Jig_No, @Channel, @Spec_UL, @Spec_LL, @Leak_Value, @Result);";
try
{
+ var maker = loginInfo?.Maker ?? string.Empty;
+ var model = loginInfo?.Model ?? string.Empty;
+ var variant1 = loginInfo?.Variant1 ?? string.Empty;
+ var variant2 = loginInfo?.Variant2 ?? string.Empty;
+ var operatorId = loginInfo?.Operator ?? string.Empty;
+ var lineNo = loginInfo?.LineNo ?? string.Empty;
+ var lotNo = loginInfo?.LotNo ?? string.Empty;
+ var jigNo = loginInfo?.JigNo ?? string.Empty;
+
using (var conn = new SqlConnection(_connectionString))
{
using (var cmd = new SqlCommand(query, conn))
{
- cmd.Parameters.AddWithValue("@ID", string.IsNullOrEmpty(inspectData.ProductId) ? "-" : inspectData.ProductId);
+ cmd.Parameters.AddWithValue("@IC_SN", string.IsNullOrEmpty(inspectData.IcSn) ? "-" : inspectData.IcSn);
+ cmd.Parameters.AddWithValue("@PCB_Barcode", string.IsNullOrEmpty(inspectData.PcbBarcode) ? "-" : inspectData.PcbBarcode);
+ cmd.Parameters.AddWithValue("@Maker", maker);
+ cmd.Parameters.AddWithValue("@Model", model);
+ cmd.Parameters.AddWithValue("@Variant_1", variant1);
+ cmd.Parameters.AddWithValue("@Variant_2", variant2);
+ cmd.Parameters.AddWithValue("@Operator", operatorId);
+ cmd.Parameters.AddWithValue("@Production_Date", inspectData.ProductionDate ?? DateTime.Now);
+ cmd.Parameters.AddWithValue("@Line", lineNo);
+ cmd.Parameters.AddWithValue("@Lot_No", lotNo);
+ cmd.Parameters.AddWithValue("@Jig_No", jigNo);
cmd.Parameters.AddWithValue("@Channel", string.IsNullOrEmpty(inspectData.Channel) ? "N/A" : inspectData.Channel);
- cmd.Parameters.AddWithValue("@SpecUL", string.IsNullOrEmpty(inspectData.SpecUL) ? "0.00" : inspectData.SpecUL);
- cmd.Parameters.AddWithValue("@SpecLL", string.IsNullOrEmpty(inspectData.SpecLL) ? "0.00" : inspectData.SpecLL);
- cmd.Parameters.AddWithValue("@LeakValue", string.IsNullOrEmpty(inspectData.MeasuredValue) ? "0.0000" : inspectData.MeasuredValue);
+
+ // Spec_UL, Spec_LL, Leak_Value 형 변환 및 대입
+ decimal specUL = 0;
+ if (decimal.TryParse(inspectData.SpecUL, out var parsedUL)) specUL = parsedUL;
+ cmd.Parameters.AddWithValue("@Spec_UL", specUL);
+
+ decimal specLL = 0;
+ if (decimal.TryParse(inspectData.SpecLL, out var parsedLL)) specLL = parsedLL;
+ cmd.Parameters.AddWithValue("@Spec_LL", specLL);
+
+ decimal leakValue = 0;
+ if (decimal.TryParse(inspectData.MeasuredValue, out var parsedValue)) leakValue = parsedValue;
+ cmd.Parameters.AddWithValue("@Leak_Value", leakValue);
+
cmd.Parameters.AddWithValue("@Result", string.IsNullOrEmpty(inspectData.Judgment) ? "NG" : inspectData.Judgment);
- cmd.Parameters.AddWithValue("@InspectDate", string.IsNullOrEmpty(inspectData.InspectDate) ? DateTime.Now.ToString("yyyy-MM-dd") : inspectData.InspectDate);
- cmd.Parameters.AddWithValue("@InspectTime", string.IsNullOrEmpty(inspectData.InspectTime) ? DateTime.Now.ToString("HH:mm:ss") : inspectData.InspectTime);
await conn.OpenAsync();
await cmd.ExecuteNonQueryAsync();
- Console.WriteLine($"[DB SUCCESS] EOL Data saved/updated for ID: {inspectData.ProductId}");
+ Console.WriteLine($"[DB SUCCESS] EOL Data saved/updated for IC_SN: {inspectData.ProductId}");
+ return true;
}
}
}
@@ -68,6 +106,7 @@ namespace leak_test_project.Services
{
// 실시간 검사 공정 루프 흐름이 DB 커넥션 예외로 중단되지 않도록 예외 격리 처리
Console.WriteLine($"[DB ERROR] Failed to upsert EOL Data: {ex.Message}");
+ return false;
}
}
@@ -89,8 +128,9 @@ namespace leak_test_project.Services
return true;
}
}
- catch
+ catch (Exception ex)
{
+ System.Diagnostics.Debug.WriteLine($"[DB Connection Check Error] {ex.Message}");
return false;
}
}
diff --git a/leak_test_project/Services/IIdSensorService.cs b/leak_test_project/Services/IIdSensorService.cs
index 7ffccad..5a47e3e 100644
--- a/leak_test_project/Services/IIdSensorService.cs
+++ b/leak_test_project/Services/IIdSensorService.cs
@@ -11,6 +11,6 @@ namespace leak_test_project.Services
bool Connect();
void Disconnect();
- SensorIdData ReadSensor();
+ System.Threading.Tasks.Task ReadSensorAsync();
}
}
diff --git a/leak_test_project/Services/LoginAccountRepository.cs b/leak_test_project/Services/LoginAccountRepository.cs
new file mode 100644
index 0000000..af21e2d
--- /dev/null
+++ b/leak_test_project/Services/LoginAccountRepository.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Housing.Services
+{
+ public class LoginAccountRepository
+ {
+ private readonly DatabaseSettings _dbSettings;
+ private readonly LoginSettings _loginSettings;
+
+ public LoginAccountRepository(DatabaseSettings dbSettings, LoginSettings loginSettings)
+ {
+ _dbSettings = dbSettings;
+ _loginSettings = loginSettings;
+ }
+
+ public async Task ExistsAsync(string operatorId, string password)
+ {
+ // 1. 데모 계정(test/test)인 경우 DB 조회 없이 즉시 로그인 통과
+ if (IsDefaultAccount(operatorId, password))
+ {
+ return true;
+ }
+
+ // 2. 오프라인 모드인 경우 기본 성공
+ if (_loginSettings.OfflinePreview)
+ {
+ return true;
+ }
+
+ // DB 커넥션 스트링이 비어있으면 실패 처리
+ if (string.IsNullOrWhiteSpace(_dbSettings.ConnectionString))
+ {
+ return false;
+ }
+
+ try
+ {
+ // 실제 DB에서 계정을 조회해보기 위한 시도
+ using (var conn = new System.Data.SqlClient.SqlConnection(_dbSettings.ConnectionString))
+ {
+ await conn.OpenAsync();
+
+ if (!string.IsNullOrWhiteSpace(_loginSettings.Procedure))
+ {
+ // 저장 프로시저 호출
+ using (var cmd = new System.Data.SqlClient.SqlCommand(_loginSettings.Procedure, conn))
+ {
+ cmd.CommandType = System.Data.CommandType.StoredProcedure;
+ cmd.Parameters.AddWithValue("@OperatorId", operatorId);
+ cmd.Parameters.AddWithValue("@Password", password);
+ cmd.Parameters.AddWithValue("@ProcessName", _loginSettings.ProcessName ?? string.Empty);
+
+ var result = await cmd.ExecuteScalarAsync();
+ if (result != null && result != DBNull.Value)
+ {
+ if (int.TryParse(result.ToString(), out var count))
+ {
+ return count > 0;
+ }
+ }
+ return false;
+ }
+ }
+ else
+ {
+ // 일반 쿼리 수행
+ const string query = "SELECT COUNT(*) FROM [Users] WHERE [OperatorId] = @Id AND [Password] = @Pw";
+ using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
+ {
+ cmd.Parameters.AddWithValue("@Id", operatorId);
+ cmd.Parameters.AddWithValue("@Pw", password);
+
+ var count = (int)await cmd.ExecuteScalarAsync();
+ return count > 0;
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[LoginAccountRepository ExistsAsync Error] {ex.Message}");
+ // DB 연결이나 쿼리 오류 발생 시 로그인 실패 처리
+ return false;
+ }
+ }
+
+ private bool IsDefaultAccount(string operatorId, string password)
+ {
+ // 기본 개발/테스트용 데모 계정 처리 (test/test만 허용)
+ if (string.Equals(operatorId, "test", StringComparison.OrdinalIgnoreCase) &&
+ string.Equals(password, "test", StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+}
diff --git a/leak_test_project/Services/LoginHistoryStore.cs b/leak_test_project/Services/LoginHistoryStore.cs
new file mode 100644
index 0000000..75001a2
--- /dev/null
+++ b/leak_test_project/Services/LoginHistoryStore.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+
+namespace Housing.Services
+{
+ // XML 직렬화를 위한 데이터 컨테이너 클래스 정의
+ [XmlRoot("LoginHistory")]
+ public class LoginHistoryData
+ {
+ [XmlElement("Category")]
+ public List Categories { get; set; } = new List();
+ }
+
+ public class HistoryCategory
+ {
+ [XmlAttribute("Name")]
+ public string Name { get; set; }
+
+ [XmlElement("Value")]
+ public List Values { get; set; } = new List();
+ }
+
+ public class LoginHistoryStore
+ {
+ private readonly string _filePath;
+ private readonly Dictionary> _history = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
+ private LoginHistoryStore(string filePath)
+ {
+ _filePath = filePath;
+ }
+
+ public static LoginHistoryStore Load(string path)
+ {
+ var store = new LoginHistoryStore(path);
+ store.LoadFromFile();
+ return store;
+ }
+
+ public List GetValues(string fieldName)
+ {
+ if (_history.TryGetValue(fieldName, out var list))
+ {
+ return new List(list);
+ }
+ return new List();
+ }
+
+ public void Remember(string fieldName, string value)
+ {
+ if (string.IsNullOrWhiteSpace(value)) return;
+ value = value.Trim();
+
+ if (!_history.TryGetValue(fieldName, out var list))
+ {
+ list = new List();
+ _history[fieldName] = list;
+ }
+
+ // 중복 제거 및 맨 앞으로 이동
+ list.RemoveAll(x => string.Equals(x, value, StringComparison.OrdinalIgnoreCase));
+ list.Insert(0, value);
+
+ // 최대 5개 이력 유지
+ if (list.Count > 5)
+ {
+ list.RemoveRange(5, list.Count - 5);
+ }
+ }
+
+ public bool Forget(string fieldName, string value)
+ {
+ if (string.IsNullOrWhiteSpace(value)) return false;
+ value = value.Trim();
+
+ if (_history.TryGetValue(fieldName, out var list))
+ {
+ bool removed = list.RemoveAll(x => string.Equals(x, value, StringComparison.OrdinalIgnoreCase)) > 0;
+ return removed;
+ }
+ return false;
+ }
+
+ public void Save()
+ {
+ try
+ {
+ var dir = Path.GetDirectoryName(_filePath);
+ if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
+ {
+ Directory.CreateDirectory(dir);
+ }
+
+ var data = new LoginHistoryData();
+ foreach (var kvp in _history)
+ {
+ data.Categories.Add(new HistoryCategory
+ {
+ Name = kvp.Key,
+ Values = kvp.Value
+ });
+ }
+
+ XmlSerializer serializer = new XmlSerializer(typeof(LoginHistoryData));
+ using (var sw = new StreamWriter(_filePath))
+ {
+ serializer.Serialize(sw, data);
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[LoginHistoryStore Save Error] {ex.Message}");
+ }
+ }
+
+ private void LoadFromFile()
+ {
+ try
+ {
+ if (!File.Exists(_filePath)) return;
+
+ XmlSerializer serializer = new XmlSerializer(typeof(LoginHistoryData));
+ using (var sr = new StreamReader(_filePath))
+ {
+ var data = (LoginHistoryData)serializer.Deserialize(sr);
+ if (data?.Categories != null)
+ {
+ _history.Clear();
+ foreach (var cat in data.Categories)
+ {
+ if (!string.IsNullOrEmpty(cat.Name))
+ {
+ _history[cat.Name] = cat.Values ?? new List();
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[LoginHistoryStore Load Error] {ex.Message}");
+ }
+ }
+ }
+}
diff --git a/leak_test_project/Services/LoginServicesStub.cs b/leak_test_project/Services/LoginServicesStub.cs
deleted file mode 100644
index f0f945c..0000000
--- a/leak_test_project/Services/LoginServicesStub.cs
+++ /dev/null
@@ -1,295 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Text.RegularExpressions;
-using System.Threading.Tasks;
-
-namespace Housing.Services
-{
- // 1. DatabaseSettings
- public class DatabaseSettings
- {
- public string ConnectionString { get; set; } = string.Empty;
-
- public static DatabaseSettings Load(string path)
- {
- var settings = new DatabaseSettings();
- try
- {
- if (File.Exists(path))
- {
- var lines = File.ReadAllLines(path);
- 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)
- {
- settings.ConnectionString = parts[1].Trim();
- }
- }
- }
- }
- }
- catch
- {
- // 로드 실패 시 무시
- }
- return settings;
- }
- }
-
- // 2. LoginSettings
- public class LoginSettings
- {
- public bool OfflinePreview { get; set; } = true; // 기본값은 오프라인 테스트 편의를 위해 true
-
- public static LoginSettings Load(string path)
- {
- var settings = new LoginSettings();
- try
- {
- if (File.Exists(path))
- {
- var lines = File.ReadAllLines(path);
- foreach (var line in lines)
- {
- var trimmed = line.Trim();
- if (trimmed.StartsWith("OfflinePreview", StringComparison.OrdinalIgnoreCase))
- {
- var parts = trimmed.Split(new[] { '=' }, 2);
- if (parts.Length == 2)
- {
- if (bool.TryParse(parts[1].Trim(), out var val))
- {
- settings.OfflinePreview = val;
- }
- else if (int.TryParse(parts[1].Trim(), out var intVal))
- {
- settings.OfflinePreview = intVal != 0;
- }
- }
- }
- }
- }
- }
- catch
- {
- // 로드 실패 시 무시
- }
- return settings;
- }
- }
-
- // 3. LoginHistoryStore
- public class LoginHistoryStore
- {
- private readonly string _filePath;
- private readonly Dictionary> _history = new Dictionary>(StringComparer.OrdinalIgnoreCase);
-
- private LoginHistoryStore(string filePath)
- {
- _filePath = filePath;
- }
-
- public static LoginHistoryStore Load(string path)
- {
- var store = new LoginHistoryStore(path);
- store.LoadFromFile();
- return store;
- }
-
- public List GetValues(string fieldName)
- {
- if (_history.TryGetValue(fieldName, out var list))
- {
- return new List(list);
- }
- return new List();
- }
-
- public void Remember(string fieldName, string value)
- {
- if (string.IsNullOrWhiteSpace(value)) return;
- value = value.Trim();
-
- if (!_history.TryGetValue(fieldName, out var list))
- {
- list = new List();
- _history[fieldName] = list;
- }
-
- // 중복 제거 및 맨 앞으로 이동
- list.RemoveAll(x => string.Equals(x, value, StringComparison.OrdinalIgnoreCase));
- list.Insert(0, value);
-
- // 최대 5개 이력 유지
- if (list.Count > 5)
- {
- list.RemoveRange(5, list.Count - 5);
- }
- }
-
- public bool Forget(string fieldName, string value)
- {
- if (string.IsNullOrWhiteSpace(value)) return false;
- value = value.Trim();
-
- if (_history.TryGetValue(fieldName, out var list))
- {
- bool removed = list.RemoveAll(x => string.Equals(x, value, StringComparison.OrdinalIgnoreCase)) > 0;
- return removed;
- }
- return false;
- }
-
- public void Save()
- {
- try
- {
- var dir = Path.GetDirectoryName(_filePath);
- if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
- {
- Directory.CreateDirectory(dir);
- }
-
- // 초간단 JSON 형식으로 직렬화 (의존성 최소화)
- using (var sw = new StreamWriter(_filePath))
- {
- sw.WriteLine("{");
- var keys = new List(_history.Keys);
- for (int i = 0; i < keys.Count; i++)
- {
- var key = keys[i];
- var values = _history[key];
- var arrayItems = new List();
- foreach (var val in values)
- {
- // 따옴표 escape
- var escaped = val.Replace("\"", "\\\"");
- arrayItems.Add($"\"{escaped}\"");
- }
- var line = $" \"{key}\": [{string.Join(", ", arrayItems)}]";
- if (i < keys.Count - 1)
- {
- line += ",";
- }
- sw.WriteLine(line);
- }
- sw.WriteLine("}");
- }
- }
- catch
- {
- // 저장 오류 시 무시
- }
- }
-
- private void LoadFromFile()
- {
- try
- {
- if (!File.Exists(_filePath)) return;
-
- var text = File.ReadAllText(_filePath);
- // 정규식이나 아주 심플한 파싱으로 키와 배열 파싱
- // 형식 예: "Maker": ["MOBI", "TEST"]
- var matches = Regex.Matches(text, @"""([^""]+)""\s*:\s*\[([^\]]*)\]");
- foreach (Match match in matches)
- {
- var key = match.Groups[1].Value;
- var arrayContent = match.Groups[2].Value;
- var valueMatches = Regex.Matches(arrayContent, @"""([^""]*)""");
-
- var list = new List();
- foreach (Match valMatch in valueMatches)
- {
- list.Add(valMatch.Groups[1].Value);
- }
- _history[key] = list;
- }
- }
- catch
- {
- // 로드 오류 시 무시
- }
- }
- }
-
- // 4. LoginAccountRepository
- public class LoginAccountRepository
- {
- private readonly DatabaseSettings _dbSettings;
- private readonly LoginSettings _loginSettings;
-
- public LoginAccountRepository(DatabaseSettings dbSettings, LoginSettings loginSettings)
- {
- _dbSettings = dbSettings;
- _loginSettings = loginSettings;
- }
-
- public async Task ExistsAsync(string operatorId, string password)
- {
- // 오프라인 모드인 경우 기본 성공
- if (_loginSettings.OfflinePreview)
- {
- return true;
- }
-
- // DB 커넥션 스트링이 비어있으면 기본값 확인
- if (string.IsNullOrWhiteSpace(_dbSettings.ConnectionString))
- {
- return IsDefaultAccount(operatorId, password);
- }
-
- try
- {
- // 실제 DB에서 계정을 조회해보기 위한 시도
- using (var conn = new System.Data.SqlClient.SqlConnection(_dbSettings.ConnectionString))
- {
- await conn.OpenAsync();
-
- // 우선 Users 또는 Accounts와 같이 로그인 테이블명이 뭔지 모를 수 있으므로
- // 쿼리 실패 시 default 계정 검증으로 fallback 하도록 구성
- const string query = "SELECT COUNT(*) FROM [Users] WHERE [OperatorId] = @Id AND [Password] = @Pw";
- using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
- {
- cmd.Parameters.AddWithValue("@Id", operatorId);
- cmd.Parameters.AddWithValue("@Pw", password);
-
- var count = (int)await cmd.ExecuteScalarAsync();
- return count > 0;
- }
- }
- }
- catch
- {
- // DB 연결이나 쿼리 오류 발생 시, 테스트 편의를 위해 test/test 혹은 admin/admin 등 디폴트 계정을 제공
- return IsDefaultAccount(operatorId, password);
- }
- }
-
- private bool IsDefaultAccount(string operatorId, string password)
- {
- // 기본 개발/테스트용 계정 처리
- if (string.Equals(operatorId, "test", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(password, "test", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- if (string.Equals(operatorId, "admin", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(password, "admin", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- if (string.Equals(operatorId, "su", StringComparison.OrdinalIgnoreCase) &&
- string.Equals(password, "su", StringComparison.OrdinalIgnoreCase))
- {
- return true;
- }
- return false;
- }
- }
-}
diff --git a/leak_test_project/Services/LoginSettings.cs b/leak_test_project/Services/LoginSettings.cs
new file mode 100644
index 0000000..f963a05
--- /dev/null
+++ b/leak_test_project/Services/LoginSettings.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+
+namespace Housing.Services
+{
+ public class LoginSettings
+ {
+ public bool OfflinePreview { get; set; } = true; // 기본값은 오프라인 테스트 편의를 위해 true
+ public string Procedure { get; set; } = string.Empty;
+ public string ProcessName { get; set; } = string.Empty;
+
+ private static readonly object _loginLock = new object();
+ private static LoginSettings _cachedSettings;
+ private static DateTime _lastWriteTime;
+ private static string _cachedPath;
+
+ public static LoginSettings Load(string path)
+ {
+ lock (_loginLock)
+ {
+ try
+ {
+ if (File.Exists(path))
+ {
+ var currentWriteTime = File.GetLastWriteTime(path);
+ if (_cachedSettings != null && _cachedPath == path && _lastWriteTime == currentWriteTime)
+ {
+ return _cachedSettings;
+ }
+
+ var settings = new LoginSettings();
+ var lines = File.ReadAllLines(path);
+ foreach (var line in lines)
+ {
+ var trimmed = line.Trim();
+ if (trimmed.StartsWith("OfflinePreview", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ if (bool.TryParse(parts[1].Trim(), out var val))
+ {
+ settings.OfflinePreview = val;
+ }
+ else if (int.TryParse(parts[1].Trim(), out var intVal))
+ {
+ settings.OfflinePreview = intVal != 0;
+ }
+ }
+ }
+ else if (trimmed.StartsWith("Procedure", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ settings.Procedure = parts[1].Trim();
+ }
+ }
+ else if (trimmed.StartsWith("ProcessName", StringComparison.OrdinalIgnoreCase))
+ {
+ var parts = trimmed.Split(new[] { '=' }, 2);
+ if (parts.Length == 2)
+ {
+ settings.ProcessName = parts[1].Trim();
+ }
+ }
+ }
+ _cachedSettings = settings;
+ _lastWriteTime = currentWriteTime;
+ _cachedPath = path;
+ return settings;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[LoginSettings Load Error] {ex.Message}");
+ }
+ return _cachedSettings ?? new LoginSettings();
+ }
+ }
+ }
+}
diff --git a/leak_test_project/Services/TestProcessService.cs b/leak_test_project/Services/TestProcessService.cs
index b94588c..a8e99bd 100644
--- a/leak_test_project/Services/TestProcessService.cs
+++ b/leak_test_project/Services/TestProcessService.cs
@@ -42,6 +42,7 @@ namespace leak_test_project.Services
private readonly IIdSensorService _leftSensor;
private readonly IIdSensorService _rightSensor;
private readonly SentinelC28Service _sentinelService;
+ private readonly Housing.Login.StartupLoginWindowResult _loginInfo;
private Thread _leftThread;
private Thread _rightThread;
@@ -50,6 +51,10 @@ namespace leak_test_project.Services
private volatile bool _running = true;
private readonly SemaphoreSlim _testLock = new SemaphoreSlim(1, 1);
+ // DIO 시작 신호를 폴링 대신 대기하기 위한 이벤트
+ private readonly AutoResetEvent _leftStartEvent = new AutoResetEvent(false);
+ private readonly AutoResetEvent _rightStartEvent = new AutoResetEvent(false);
+
// LEAK 시험 결과를 수신하기 위한 필드
private ParsedData _leftResult;
private ParsedData _rightResult;
@@ -79,12 +84,14 @@ namespace leak_test_project.Services
IDioBoard dioBoard,
IIdSensorService leftSensor,
IIdSensorService rightSensor,
- SentinelC28Service sentinelService)
+ SentinelC28Service sentinelService,
+ Housing.Login.StartupLoginWindowResult loginInfo)
{
_dioBoard = dioBoard;
_leftSensor = leftSensor;
_rightSensor = rightSensor;
_sentinelService = sentinelService;
+ _loginInfo = loginInfo;
// DIO START 신호 이벤트 연결
_dioBoard.InputChanged += OnDioInputChanged;
@@ -110,8 +117,16 @@ namespace leak_test_project.Services
/// 시뮬레이션용: 수동으로 시험 시작 트리거
public void TriggerTestStart(int testIndex)
{
- if (testIndex == 0) _leftTestStart = true;
- else _rightTestStart = true;
+ if (testIndex == 0)
+ {
+ _leftTestStart = true;
+ _leftStartEvent.Set();
+ }
+ else
+ {
+ _rightTestStart = true;
+ _rightStartEvent.Set();
+ }
}
///
@@ -140,19 +155,14 @@ namespace leak_test_project.Services
var sensor = isLeft ? _leftSensor : _rightSensor;
- // 시리얼 통신이므로 백그라운드에서 실행
- var sensorData = await System.Threading.Tasks.Task.Run(() => sensor.ReadSensor());
+ // 비동기로 센서 정보 판독
+ var sensorData = await sensor.ReadSensorAsync();
if (sensorData != null)
{
SensorReadComplete?.Invoke(this, (testIndex, sensorData));
- // 실제 검사 시퀀스와 동일한 불량 제품 필터링 체크
- if (sensorData.PrevResult != "F")
- {
- NotifyError(testIndex, "불량 제품 투입 (이전 결과: " + sensorData.PrevResult + ")");
- return;
- }
+
NotifyProgress(testIndex, "ID 테스트 완료", "LightBlue");
}
@@ -160,7 +170,7 @@ namespace leak_test_project.Services
{
// 실패 시 NotifyError를 호출하지 않는 것도 실제 시퀀스와 동일 (센서 서비스가 이미 에러 이벤트 발생시킴)
// 다만 UI 갱신을 위해 빈 데이터는 전송
- sensorData = new SensorIdData { ID = "-", PrevResult = "F" };
+ sensorData = new SensorIdData { ID = "-" };
SensorReadComplete?.Invoke(this, (testIndex, sensorData));
NotifyProgress(testIndex, "ID 테스트 실패", "Red");
}
@@ -181,12 +191,14 @@ namespace leak_test_project.Services
_dioBoard.WriteOutput("LEFT_OK", false);
_dioBoard.WriteOutput("LEFT_NG", false);
_leftTestStart = true;
+ _leftStartEvent.Set();
}
else if (e.PointName == "RIGHT_START" && e.NewValue)
{
_dioBoard.WriteOutput("RIGHT_OK", false);
_dioBoard.WriteOutput("RIGHT_NG", false);
_rightTestStart = true;
+ _rightStartEvent.Set();
}
}
@@ -198,10 +210,14 @@ namespace leak_test_project.Services
{
bool isLeft = (testIndex == 0);
string side = isLeft ? "LEFT" : "RIGHT";
+ var startEvent = isLeft ? _leftStartEvent : _rightStartEvent;
while (_running)
{
- Thread.Sleep(2);
+ // 이벤트 신호 대기 (2ms 폴링 대신 신호 수신 시까지 차단)
+ startEvent.WaitOne();
+
+ if (!_running) break;
// 시작 신호 대기
bool shouldStart = isLeft ? _leftTestStart : _rightTestStart;
@@ -237,7 +253,7 @@ namespace leak_test_project.Services
Debug.WriteLine($"[TestProcess-{side}] Phase 1: 센서 정보 읽기 시작");
NotifyProgress(testIndex, "센서 정보 읽는 중", "LightSeaGreen");
var sensor = isLeft ? _leftSensor : _rightSensor;
- var sensorData = sensor.ReadSensor();
+ var sensorData = sensor.ReadSensorAsync().GetAwaiter().GetResult();
if (sensorData != null)
{
@@ -245,19 +261,71 @@ namespace leak_test_project.Services
}
else
{
- // 센서 읽기 실패해도 시험은 계속 진행
- sensorData = new SensorIdData { ID = "-", PrevResult = "F" };
- SensorReadComplete?.Invoke(this, (testIndex, sensorData));
- }
+ NotifyError(testIndex, "ID 읽기 실패");
+ _dioBoard.WriteOutput(side + "_NG", true);
- // === 2단계: 불량 제품 필터링 ===
- Debug.WriteLine($"[TestProcess-{side}] Phase 2: 센서 ID ({sensorData?.ID}), 이전결과 ({sensorData?.PrevResult}) 필터링 확인");
- if (sensorData.PrevResult != "F")
- {
- NotifyError(testIndex, "불량 제품 투입 (이전 결과: " + sensorData.PrevResult + ")");
+ var failedSensorData = new SensorIdData { ID = "-" };
+ SensorReadComplete?.Invoke(this, (testIndex, failedSensorData));
+
+ TestCompleted?.Invoke(this, new TestResultEventArgs
+ {
+ TestIndex = testIndex,
+ SensorData = failedSensorData,
+ MeasuredValue = "0.0000",
+ Judgment = "NG",
+ SensorJudgment = "FAIL",
+ SpecMismatch = false
+ });
+
+ var failData = new InspectData
+ {
+ InspectDate = DateTime.Now.ToString("yyyy-MM-dd"),
+ InspectTime = DateTime.Now.ToString("HH:mm:ss"),
+ Channel = side,
+ ProductId = "-",
+ IcSn = "-",
+ PcbBarcode = "-",
+ MeasuredValue = "0.0000",
+ Judgment = "NG",
+ SpecUL = ConfigManager.Current.SpecUL.ToString("F2"),
+ SpecLL = ConfigManager.Current.SpecLL.ToString("F2"),
+ Maker = _loginInfo?.Maker ?? string.Empty,
+ Model = _loginInfo?.Model ?? string.Empty,
+ Variant1 = _loginInfo?.Variant1 ?? string.Empty,
+ Variant2 = _loginInfo?.Variant2 ?? string.Empty,
+ Operator = _loginInfo?.Operator ?? string.Empty,
+ Line = _loginInfo?.LineNo ?? string.Empty,
+ LotNo = _loginInfo?.LotNo ?? string.Empty,
+ JigNo = _loginInfo?.JigNo ?? string.Empty,
+ ProductionDate = DateTime.Now
+ };
+ FileLogger.LogInspectData(failData);
+
+ try
+ {
+ var dbIniPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Database.ini");
+ var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
+ var dbService = new EolDatabaseService(dbConnStr);
+ _ = Task.Run(async () =>
+ {
+ bool success = await dbService.SaveEolDataAsync(failData, _loginInfo);
+ if (!success)
+ {
+ NotifyError(testIndex, "DB 저장 실패");
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine("[DB ID Read Fail Integration Error] " + ex.Message);
+ }
+
+ NotifyProgress(testIndex, "시험 완료", "LightBlue");
continue;
}
+
+
// === 3단계: LEAK 시험 수행 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 3: LEAK 시험 대기 시작 (최대 30초)");
NotifyProgress(testIndex, "LEAK 시험중", "LightYellow");
@@ -289,18 +357,38 @@ namespace leak_test_project.Services
InspectTime = DateTime.Now.ToString("HH:mm:ss"),
Channel = side,
ProductId = sensorData.ID,
+ IcSn = sensorData.ID,
+ PcbBarcode = sensorData.ID,
MeasuredValue = "0.0000",
Judgment = "NG",
SpecUL = ConfigManager.Current.SpecUL.ToString("F2"),
- SpecLL = ConfigManager.Current.SpecLL.ToString("F2")
+ SpecLL = ConfigManager.Current.SpecLL.ToString("F2"),
+ Maker = _loginInfo?.Maker ?? string.Empty,
+ Model = _loginInfo?.Model ?? string.Empty,
+ Variant1 = _loginInfo?.Variant1 ?? string.Empty,
+ Variant2 = _loginInfo?.Variant2 ?? string.Empty,
+ Operator = _loginInfo?.Operator ?? string.Empty,
+ Line = _loginInfo?.LineNo ?? string.Empty,
+ LotNo = _loginInfo?.LotNo ?? string.Empty,
+ JigNo = _loginInfo?.JigNo ?? string.Empty,
+ ProductionDate = DateTime.Now
};
FileLogger.LogInspectData(timeoutData);
// === MSSQL 데이터베이스 비동기 저장 ===
try
{
- var dbService = new EolDatabaseService(ConfigManager.Current.DbConnectionString);
- _ = Task.Run(async () => await dbService.SaveEolDataAsync(timeoutData));
+ var dbIniPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Database.ini");
+ var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
+ var dbService = new EolDatabaseService(dbConnStr);
+ _ = Task.Run(async () =>
+ {
+ bool success = await dbService.SaveEolDataAsync(timeoutData, _loginInfo);
+ if (!success)
+ {
+ NotifyError(testIndex, "DB 저장 실패");
+ }
+ });
}
catch (Exception ex)
{
@@ -349,18 +437,38 @@ namespace leak_test_project.Services
InspectTime = DateTime.Now.ToString("HH:mm:ss"),
Channel = side,
ProductId = sensorData.ID,
+ IcSn = sensorData.ID,
+ PcbBarcode = sensorData.ID,
MeasuredValue = leakResult.MeasuredValueString,
Judgment = programJudgment,
SpecUL = specUL.ToString("F2"),
- SpecLL = specLL.ToString("F2")
+ SpecLL = specLL.ToString("F2"),
+ Maker = _loginInfo?.Maker ?? string.Empty,
+ Model = _loginInfo?.Model ?? string.Empty,
+ Variant1 = _loginInfo?.Variant1 ?? string.Empty,
+ Variant2 = _loginInfo?.Variant2 ?? string.Empty,
+ Operator = _loginInfo?.Operator ?? string.Empty,
+ Line = _loginInfo?.LineNo ?? string.Empty,
+ LotNo = _loginInfo?.LotNo ?? string.Empty,
+ JigNo = _loginInfo?.JigNo ?? string.Empty,
+ ProductionDate = DateTime.Now
};
FileLogger.LogInspectData(inspectData);
// === MSSQL 데이터베이스 비동기 저장 ===
try
{
- var dbService = new EolDatabaseService(ConfigManager.Current.DbConnectionString);
- _ = Task.Run(async () => await dbService.SaveEolDataAsync(inspectData));
+ var dbIniPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Database.ini");
+ var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
+ var dbService = new EolDatabaseService(dbConnStr);
+ _ = Task.Run(async () =>
+ {
+ bool success = await dbService.SaveEolDataAsync(inspectData, _loginInfo);
+ if (!success)
+ {
+ NotifyError(testIndex, "DB 저장 실패");
+ }
+ });
}
catch (Exception ex)
{
@@ -451,12 +559,16 @@ namespace leak_test_project.Services
_running = false;
_leftResultReady.Set(); // 대기 중인 스레드 해제
_rightResultReady.Set();
+ _leftStartEvent.Set(); // 시작 대기 해제
+ _rightStartEvent.Set();
_dioBoard.InputChanged -= OnDioInputChanged;
_sentinelService.OnFinalResultParsed -= OnSentinelFinalResult;
_leftResultReady?.Dispose();
_rightResultReady?.Dispose();
+ _leftStartEvent?.Dispose();
+ _rightStartEvent?.Dispose();
Console.WriteLine("[TestProcess] Disposed.");
}
diff --git a/leak_test_project/Utils/ConfigHelper.cs b/leak_test_project/Utils/ConfigHelper.cs
index bb0c61c..018a492 100644
--- a/leak_test_project/Utils/ConfigHelper.cs
+++ b/leak_test_project/Utils/ConfigHelper.cs
@@ -47,9 +47,21 @@ namespace leak_test_project.Utils
try
{
XmlSerializer serializer = new XmlSerializer(typeof(AppConfig));
- using (FileStream fs = new FileStream(ConfigPath, FileMode.Create))
+ var settings = new System.Xml.XmlWriterSettings
{
- serializer.Serialize(fs, Current);
+ Indent = true,
+ IndentChars = " ",
+ NewLineChars = "\r\n",
+ NewLineHandling = System.Xml.NewLineHandling.Replace
+ };
+
+ // 네임스페이스 생략하여 XML 가독성 개선
+ var namespaces = new XmlSerializerNamespaces();
+ namespaces.Add(string.Empty, string.Empty);
+
+ using (var writer = System.Xml.XmlWriter.Create(ConfigPath, settings))
+ {
+ serializer.Serialize(writer, Current, namespaces);
}
ConfigChanged?.Invoke(null, EventArgs.Empty);
}
@@ -143,22 +155,28 @@ namespace leak_test_project.Utils
using (var sw = new StreamWriter(filePath))
{
+ sw.WriteLine("; =====================================================================");
+ sw.WriteLine("; 하드웨어 디지털 입출력 설정 파일 (DioConfig.ini)");
+ sw.WriteLine("; =====================================================================");
+ sw.WriteLine();
sw.WriteLine("[DIO]");
- sw.WriteLine($"CompanyName={CompanyName}");
- sw.WriteLine("BoardCnt=1");
- sw.WriteLine("LogEnabled=False");
+ sw.WriteLine($"CompanyName={CompanyName} ; 입출력 보드 제조사명 (예: ADLink)");
+ sw.WriteLine("BoardCnt=1 ; 설치된 총 입출력 보드 개수");
+ sw.WriteLine("LogEnabled=False ; DIO 동작 로그 기록 여부 (True / False)");
sw.WriteLine();
sw.WriteLine("[BOARD_1]");
- sw.WriteLine($"BoardType={BoardType}");
- sw.WriteLine($"BoardNumber={BoardNumber}");
- sw.WriteLine($"InPortCnt={InputPoints.Count}");
- sw.WriteLine($"OutPortCnt={OutputPoints.Count}");
+ sw.WriteLine($"BoardType={BoardType} ; ADLINK DIO 보드 하드웨어 모델명");
+ sw.WriteLine($"BoardNumber={BoardNumber} ; 시스템 내 보드 식별 번호 (기본 0)");
+ sw.WriteLine($"InPortCnt={InputPoints.Count} ; 입력에 사용할 접점 포트 개수");
+ sw.WriteLine($"OutPortCnt={OutputPoints.Count} ; 출력에 사용할 접점 포트 개수");
sw.WriteLine();
sw.WriteLine("[BOARD_1_IN]");
+ sw.WriteLine("; [핀 번호]=[기능 이름],[신호 레벨],[설명]");
for (int i = 0; i < InputPoints.Count; i++)
sw.WriteLine($"{i}={InputPoints[i].Name},1,{(string.IsNullOrEmpty(InputPoints[i].Description) ? "" : InputPoints[i].Description)}");
sw.WriteLine();
sw.WriteLine("[BOARD_1_OUT]");
+ sw.WriteLine("; [핀 번호]=[기능 이름],[신호 레벨],[설명]");
for (int i = 0; i < OutputPoints.Count; i++)
sw.WriteLine($"{i}={OutputPoints[i].Name},1,{(string.IsNullOrEmpty(OutputPoints[i].Description) ? "" : OutputPoints[i].Description)}");
}
diff --git a/leak_test_project/Utils/DialogService.cs b/leak_test_project/Utils/DialogService.cs
new file mode 100644
index 0000000..2ef44b6
--- /dev/null
+++ b/leak_test_project/Utils/DialogService.cs
@@ -0,0 +1,23 @@
+using System.Windows;
+
+namespace leak_test_project.Utils
+{
+ public interface IDialogService
+ {
+ void ShowMessage(string message, string title = "알림");
+ void ShowWarning(string message, string title = "경고");
+ }
+
+ public class DefaultDialogService : IDialogService
+ {
+ public void ShowMessage(string message, string title = "알림")
+ {
+ MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Information);
+ }
+
+ public void ShowWarning(string message, string title = "경고")
+ {
+ MessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Warning);
+ }
+ }
+}
diff --git a/leak_test_project/Utils/LogUtils.cs b/leak_test_project/Utils/LogUtils.cs
index 6a77ba8..5297410 100644
--- a/leak_test_project/Utils/LogUtils.cs
+++ b/leak_test_project/Utils/LogUtils.cs
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
-using System.Windows;
using leak_test_project.Models;
namespace leak_test_project.Utils
@@ -18,7 +17,7 @@ namespace leak_test_project.Utils
private static readonly object _lock = new object();
///
- /// 검사 데이터를 Logs/yyyy-MM-dd.csv 파일에 저장함
+ /// 검사 데이터를 Logs/yyyy-MM-dd.csv 파일에 저장함 (16컬럼)
///
public static void LogInspectData(InspectData data)
{
@@ -34,14 +33,22 @@ namespace leak_test_project.Utils
bool isNewFile = !File.Exists(filePath);
- // CSV 헤더: Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment
+ // 신규 CSV 헤더: DB 스키마 16개 컬럼과 일치
if (isNewFile)
{
- string header = "Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment" + Environment.NewLine;
+ string header = "IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,Lot_No,Jig_No,Channel,Spec_UL,Spec_LL,Leak_Value,Result" + Environment.NewLine;
File.WriteAllText(filePath, header, Encoding.UTF8);
}
- string csvLine = $"{Esc(data.InspectDate)},{Esc(data.InspectTime)},{Esc(data.ProductId)},{Esc(data.Channel)}," +
+ string prodDateStr = data.ProductionDate?.ToString("yyyy-MM-dd HH:mm:ss") ?? $"{data.InspectDate} {data.InspectTime}".Trim();
+ if (string.IsNullOrEmpty(prodDateStr))
+ {
+ prodDateStr = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
+ }
+
+ string csvLine = $"{Esc(data.IcSn)},{Esc(data.PcbBarcode)},{Esc(data.Maker)},{Esc(data.Model)}," +
+ $"{Esc(data.Variant1)},{Esc(data.Variant2)},{Esc(data.Operator)},{Esc(prodDateStr)}," +
+ $"{Esc(data.Line)},{Esc(data.LotNo)},{Esc(data.JigNo)},{Esc(data.Channel)}," +
$"{Esc(data.SpecUL)},{Esc(data.SpecLL)},{Esc(data.MeasuredValue)},{Esc(data.Judgment)}{Environment.NewLine}";
File.AppendAllText(filePath, csvLine, Encoding.UTF8);
@@ -93,21 +100,165 @@ namespace leak_test_project.Utils
{
private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
- public static List ParseLogs(DateTime start, DateTime end, string judgmentFilter = "[전체]", string serialFilter = "")
+ public static List ParseLogs(DateTime start, DateTime end, string judgmentFilter = "[전체]", string serialFilter = "", string opFilter = "", string lotFilter = "", string modelFilter = "")
+ {
+ var results = new List();
+ string dbIniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Database.ini");
+ string connectionString = string.Empty;
+
+ try
+ {
+ connectionString = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
+ }
+ catch
+ {
+ // 로드 실패 시 무시
+ }
+
+ if (string.IsNullOrWhiteSpace(connectionString))
+ {
+ // DB 연결 주소가 정의되지 않은 경우 로컬 CSV 탐색으로 Fallback
+ return ParseLogsFromCsv(start, end, judgmentFilter, serialFilter, opFilter, lotFilter, modelFilter);
+ }
+
+ try
+ {
+ using (var conn = new System.Data.SqlClient.SqlConnection(connectionString))
+ {
+ conn.Open();
+
+ // DATETIME2(0) 밀리초 오차 없이 안전한 인덱스 스캔을 위해 < @EndLimit 비교식 사용
+ string query = @"
+ SELECT IC_SN, PCB_Barcode, Maker, Model, Variant_1, Variant_2, Operator, Production_Date, Line, Lot_No, Jig_No, Channel, Spec_UL, Spec_LL, Leak_Value, Result
+ FROM [dbo].[EOL]
+ WHERE Production_Date >= @Start AND Production_Date < @EndLimit";
+
+ if (judgmentFilter != "[전체]")
+ {
+ query += " AND Result = @Judgment";
+ }
+ if (!string.IsNullOrEmpty(serialFilter))
+ {
+ query += " AND (IC_SN LIKE @Serial OR PCB_Barcode LIKE @Serial)";
+ }
+ if (!string.IsNullOrEmpty(opFilter))
+ {
+ query += " AND Operator LIKE @Operator";
+ }
+ if (!string.IsNullOrEmpty(lotFilter))
+ {
+ query += " AND Lot_No LIKE @LotNo";
+ }
+ if (!string.IsNullOrEmpty(modelFilter))
+ {
+ query += " AND Model LIKE @Model";
+ }
+
+ query += " ORDER BY Production_Date DESC";
+
+ using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
+ {
+ cmd.Parameters.AddWithValue("@Start", start.Date);
+ cmd.Parameters.AddWithValue("@EndLimit", end.Date.AddDays(1)); // 익일 00:00:00 미만으로 설정하여 당일 23:59:59까지 포함
+
+ if (judgmentFilter != "[전체]")
+ {
+ cmd.Parameters.AddWithValue("@Judgment", judgmentFilter);
+ }
+ if (!string.IsNullOrEmpty(serialFilter))
+ {
+ cmd.Parameters.AddWithValue("@Serial", $"%{serialFilter}%");
+ }
+ if (!string.IsNullOrEmpty(opFilter))
+ {
+ cmd.Parameters.AddWithValue("@Operator", $"%{opFilter}%");
+ }
+ if (!string.IsNullOrEmpty(lotFilter))
+ {
+ cmd.Parameters.AddWithValue("@LotNo", $"%{lotFilter}%");
+ }
+ if (!string.IsNullOrEmpty(modelFilter))
+ {
+ cmd.Parameters.AddWithValue("@Model", $"%{modelFilter}%");
+ }
+
+ using (var reader = cmd.ExecuteReader())
+ {
+ while (reader.Read())
+ {
+ var icSn = reader.IsDBNull(0) ? "" : reader.GetString(0);
+ var pcbBarcode = reader.IsDBNull(1) ? "" : reader.GetString(1);
+ var maker = reader.IsDBNull(2) ? "" : reader.GetString(2);
+ var model = reader.IsDBNull(3) ? "" : reader.GetString(3);
+ var variant1 = reader.IsDBNull(4) ? "" : reader.GetString(4);
+ var variant2 = reader.IsDBNull(5) ? "" : reader.GetString(5);
+ var op = reader.IsDBNull(6) ? "" : reader.GetString(6);
+ var prodDate = reader.IsDBNull(7) ? (DateTime?)null : reader.GetDateTime(7);
+ var line = reader.IsDBNull(8) ? "" : reader.GetString(8);
+ var lotNo = reader.IsDBNull(9) ? "" : reader.GetString(9);
+ var jigNo = reader.IsDBNull(10) ? "" : reader.GetString(10);
+ var channel = reader.IsDBNull(11) ? "" : reader.GetString(11);
+ var specUl = reader.IsDBNull(12) ? 0m : reader.GetDecimal(12);
+ var specLl = reader.IsDBNull(13) ? 0m : reader.GetDecimal(13);
+ var leakValue = reader.IsDBNull(14) ? 0m : reader.GetDecimal(14);
+ var result = reader.IsDBNull(15) ? "NG" : reader.GetString(15);
+
+ var data = new InspectData
+ {
+ IcSn = icSn,
+ PcbBarcode = pcbBarcode,
+ Maker = maker,
+ Model = model,
+ Variant1 = variant1,
+ Variant2 = variant2,
+ Operator = op,
+ ProductionDate = prodDate,
+ Line = line,
+ LotNo = lotNo,
+ JigNo = jigNo,
+ Channel = channel,
+ SpecUL = specUl.ToString("F2"),
+ SpecLL = specLl.ToString("F2"),
+ MeasuredValue = leakValue.ToString("F4"),
+ Judgment = result,
+ Retest = "N",
+
+ // 기존 레거시 UI 바인딩 호환성 유지용 매핑
+ ProductId = icSn,
+ InspectDate = prodDate?.ToString("yyyy-MM-dd") ?? "",
+ InspectTime = prodDate?.ToString("HH:mm:ss") ?? ""
+ };
+
+ results.Add(data);
+ }
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[DB LogParser Error] {ex.Message}. Falling back to CSV.");
+ // DB 접속이나 쿼리 에러 발생 시 안전하게 CSV 로그 파일 파싱으로 대체
+ return ParseLogsFromCsv(start, end, judgmentFilter, serialFilter, opFilter, lotFilter, modelFilter);
+ }
+
+ return results;
+ }
+
+ private static List ParseLogsFromCsv(DateTime start, DateTime end, string judgmentFilter, string serialFilter, string opFilter, string lotFilter, string modelFilter)
{
var results = new List();
if (!Directory.Exists(LogDirectory))
{
- // 디렉토리가 없으면 빈 리스트 반환
return results;
}
- // 날짜 범위 내의 모든 로그 파일 찾기 (.csv)
+ // 파일명의 yyyy-MM-dd 날짜 파싱 시 시스템 로캘 영향을 막기 위해 TryParseExact 사용
var files = Directory.GetFiles(LogDirectory, "*.csv")
.Where(f => {
string fileName = Path.GetFileNameWithoutExtension(f);
- if (DateTime.TryParse(fileName, out DateTime fileDate))
+ if (DateTime.TryParseExact(fileName, "yyyy-MM-dd", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime fileDate))
{
return fileDate.Date >= start.Date && fileDate.Date <= end.Date;
}
@@ -118,11 +269,21 @@ namespace leak_test_project.Utils
{
try
{
- var lines = File.ReadAllLines(file);
- if (lines.Length <= 1) continue; // Header only or empty
+ // 엑셀 등으로 파일이 열려 파일 락(Lock)이 걸려있어도 안전하게 읽기 위해 FileShare.ReadWrite 적용
+ var linesList = new List();
+ using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
+ using (var reader = new StreamReader(fs, Encoding.UTF8))
+ {
+ string lineVal;
+ while ((lineVal = reader.ReadLine()) != null)
+ {
+ linesList.Add(lineVal);
+ }
+ }
+
+ var lines = linesList.ToArray();
+ if (lines.Length <= 1) continue;
- // CSV 헤더 (구형): Date,Time,Channel,ID,Value,Judgment,Mode,LineNo,ProductType,SpecUL,SpecLL,Retest
- // CSV 헤더 (신형): Date,Time,Channel,ID,Value,Judgment,SpecUL,SpecLL
for (int i = 1; i < lines.Length; i++)
{
string line = lines[i];
@@ -130,43 +291,157 @@ namespace leak_test_project.Utils
var parts = line.Split(',');
if (parts.Length < 8) continue;
- string date = parts[0].Trim();
- string time = parts[1].Trim();
-
- // 하위호환성 제거: 신형 8컬럼 순서(Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment)로 다이렉트 파싱
- string id = parts[2].Trim();
- string channel = parts[3].Trim();
- string specUl = parts[4].Trim();
- string specLl = parts[5].Trim();
- string val = parts[6].Trim();
- string judg = parts[7].Trim();
- string retest = "N";
-
- // 필터 적용
- if (judgmentFilter != "[전체]" && judg != judgmentFilter) continue;
- if (!string.IsNullOrEmpty(serialFilter) && !id.Contains(serialFilter)) continue;
-
- results.Add(new InspectData
+ InspectData data = null;
+
+ if (parts.Length >= 16)
+ {
+ // 16개 컬럼 신규 포맷
+ // IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,Lot_No,Jig_No,Channel,Spec_UL,Spec_LL,Leak_Value,Result
+ string icSn = parts[0].Trim();
+ string pcbBarcode = parts[1].Trim();
+ string maker = parts[2].Trim();
+ string model = parts[3].Trim();
+ string variant1 = parts[4].Trim();
+ string variant2 = parts[5].Trim();
+ string op = parts[6].Trim();
+ string prodDateRaw = parts[7].Trim();
+ string lineVal = parts[8].Trim();
+ string lotNo = parts[9].Trim();
+ string jigNo = parts[10].Trim();
+ string channel = parts[11].Trim();
+ string specUl = parts[12].Trim();
+ string specLl = parts[13].Trim();
+ string val = parts[14].Trim();
+ string judg = parts[15].Trim();
+
+ DateTime? prodDate = null;
+ if (DateTime.TryParseExact(prodDateRaw, "yyyy-MM-dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime dt))
+ {
+ prodDate = dt;
+ }
+ else if (DateTime.TryParse(prodDateRaw, out DateTime dtFallback))
+ {
+ prodDate = dtFallback;
+ }
+
+ // 필터링 적용 (부분 일치 LIKE 검색 흉내)
+ if (judgmentFilter != "[전체]" && judg != judgmentFilter) continue;
+ if (!string.IsNullOrEmpty(serialFilter) && !icSn.Contains(serialFilter) && !pcbBarcode.Contains(serialFilter)) continue;
+ if (!string.IsNullOrEmpty(opFilter) && !op.Contains(opFilter)) continue;
+ if (!string.IsNullOrEmpty(lotFilter) && !lotNo.Contains(lotFilter)) continue;
+ if (!string.IsNullOrEmpty(modelFilter) && !model.Contains(modelFilter)) continue;
+
+ data = new InspectData
+ {
+ IcSn = icSn,
+ PcbBarcode = pcbBarcode,
+ Maker = maker,
+ Model = model,
+ Variant1 = variant1,
+ Variant2 = variant2,
+ Operator = op,
+ ProductionDate = prodDate,
+ Line = lineVal,
+ LotNo = lotNo,
+ JigNo = jigNo,
+ Channel = channel,
+ SpecUL = specUl,
+ SpecLL = specLl,
+ MeasuredValue = val,
+ Judgment = judg,
+ Retest = "N",
+
+ // 기존 UI 및 구형 바인딩 호환성용 매핑
+ ProductId = icSn,
+ InspectDate = prodDate?.ToString("yyyy-MM-dd") ?? "",
+ InspectTime = prodDate?.ToString("HH:mm:ss") ?? ""
+ };
+ }
+ else
+ {
+ // 8개 컬럼 구형 포맷
+ // Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment
+ string date = parts[0].Trim();
+ string time = parts[1].Trim();
+ string id = parts[2].Trim();
+ string channel = parts[3].Trim();
+ string specUl = parts[4].Trim();
+ string specLl = parts[5].Trim();
+ string val = parts[6].Trim();
+ string judg = parts[7].Trim();
+
+ // 필터링 적용 (구형 포맷에는 Operator, LotNo, Model이 없으므로 필터값이 있는 경우 결과에서 제외)
+ if (judgmentFilter != "[전체]" && judg != judgmentFilter) continue;
+ if (!string.IsNullOrEmpty(serialFilter) && !id.Contains(serialFilter)) continue;
+ if (!string.IsNullOrEmpty(opFilter)) continue;
+ if (!string.IsNullOrEmpty(lotFilter)) continue;
+ if (!string.IsNullOrEmpty(modelFilter)) continue;
+
+ DateTime? parsedProdDate = null;
+ string dateTimeStr = $"{date} {time}";
+ if (DateTime.TryParseExact(dateTimeStr, new[] { "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm" }, System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out DateTime dtOld))
+ {
+ parsedProdDate = dtOld;
+ }
+ else if (DateTime.TryParse(dateTimeStr, out DateTime dtOldFallback))
+ {
+ parsedProdDate = dtOldFallback;
+ }
+
+ data = new InspectData
+ {
+ InspectDate = date,
+ InspectTime = time,
+ Channel = channel,
+ ProductId = id,
+ MeasuredValue = val,
+ Judgment = judg,
+ SpecUL = specUl,
+ SpecLL = specLl,
+ Retest = "N",
+
+ // 정합성을 위해 호환 필드 매핑
+ IcSn = id,
+ PcbBarcode = id,
+ ProductionDate = parsedProdDate
+ };
+ }
+
+ if (data != null)
{
- InspectDate = date,
- InspectTime = time,
- Channel = channel,
- ProductId = id,
- MeasuredValue = val,
- Judgment = judg,
- SpecUL = specUl,
- SpecLL = specLl,
- Retest = retest
- });
+ results.Add(data);
+ }
}
}
catch (Exception ex)
{
- Console.WriteLine($"[LogParser] Error reading file {file}: {ex.Message}");
+ Console.WriteLine($"[LogParser CSV Fallback] Error reading file {file}: {ex.Message}");
+ }
+ }
+
+ var sorted = results.OrderByDescending(r => r.InspectDate).ThenByDescending(r => r.InspectTime).ToList();
+ var uniqueResults = new List();
+ var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var item in sorted)
+ {
+ string id = string.IsNullOrEmpty(item.IcSn) ? item.ProductId : item.IcSn;
+ if (string.IsNullOrEmpty(id) || id == "-")
+ {
+ // 바코드가 없거나 식별 불가한 데이터는 중복 필터링 없이 그대로 출력
+ uniqueResults.Add(item);
+ }
+ else
+ {
+ // 이미 정렬되어 내림차순이므로 최초로 추가된 건(가장 최신 검사)만 유지
+ if (seenIds.Add(id))
+ {
+ uniqueResults.Add(item);
+ }
}
}
- return results.OrderByDescending(r => r.InspectDate).ThenByDescending(r => r.InspectTime).ToList();
+ return uniqueResults;
}
}
@@ -216,7 +491,7 @@ namespace leak_test_project.Utils
}
catch (Exception ex)
{
- MessageBox.Show($"CSV 저장 실패: {ex.Message}");
+ System.Diagnostics.Debug.WriteLine($"[CsvExporter ExportToCsv Error] {ex.Message}");
return false;
}
}
diff --git a/leak_test_project/ViewModels/DataViewModel.cs b/leak_test_project/ViewModels/DataViewModel.cs
index 848beb5..05fbf43 100644
--- a/leak_test_project/ViewModels/DataViewModel.cs
+++ b/leak_test_project/ViewModels/DataViewModel.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
-using System.Windows;
using System.Windows.Input;
using Microsoft.Win32;
using leak_test_project.Models;
@@ -27,6 +26,15 @@ namespace leak_test_project.ViewModels
private string _idFilter = "";
public string IdFilter { get => _idFilter; set => SetProperty(ref _idFilter, value); }
+ private string _operatorFilter = "";
+ public string OperatorFilter { get => _operatorFilter; set => SetProperty(ref _operatorFilter, value); }
+
+ private string _lotNoFilter = "";
+ public string LotNoFilter { get => _lotNoFilter; set => SetProperty(ref _lotNoFilter, value); }
+
+ private string _modelFilter = "";
+ public string ModelFilter { get => _modelFilter; set => SetProperty(ref _modelFilter, value); }
+
private List _searchResults = new List();
public List SearchResults { get => _searchResults; set => SetProperty(ref _searchResults, value); }
@@ -37,14 +45,17 @@ namespace leak_test_project.ViewModels
private int _resultCount = 0;
public int ResultCount { get => _resultCount; set => SetProperty(ref _resultCount, value); }
+ private readonly IDialogService _dialogService;
+
public ICommand SearchCommand { get; }
public ICommand SaveCsvCommand { get; }
/// Close 버튼 시 홈으로 돌아가는 커맨드
public ICommand CloseCommand { get; }
- public DataViewModel(Action navigateHome)
+ public DataViewModel(Action navigateHome, IDialogService dialogService = null)
{
+ _dialogService = dialogService ?? new DefaultDialogService();
SearchCommand = new RelayCommand(o => ExecuteSearch());
SaveCsvCommand = new RelayCommand(o => ExecuteSaveCsv());
CloseCommand = new RelayCommand(o => navigateHome?.Invoke());
@@ -74,7 +85,15 @@ namespace leak_test_project.ViewModels
? judgmentOptions[JudgmentIndex]
: "[전체]";
- var results = LogParser.ParseLogs(StartDate, EndDate, judgment, IdFilter?.Trim() ?? "");
+ var results = LogParser.ParseLogs(
+ StartDate,
+ EndDate,
+ judgment,
+ IdFilter?.Trim() ?? "",
+ OperatorFilter?.Trim() ?? "",
+ LotNoFilter?.Trim() ?? "",
+ ModelFilter?.Trim() ?? ""
+ );
SearchResults = results;
ResultCount = results.Count;
@@ -82,7 +101,7 @@ namespace leak_test_project.ViewModels
if (results.Count == 0)
{
- MessageBox.Show("해당 조건의 데이터가 없습니다.");
+ _dialogService.ShowWarning("해당 조건의 데이터가 없습니다.");
}
}
@@ -92,7 +111,7 @@ namespace leak_test_project.ViewModels
// 빈 행이 포함되지 않은 원본 검색 결과(SearchResults)만 저장
if (SearchResults == null || SearchResults.Count == 0)
{
- MessageBox.Show("저장할 데이터가 없습니다.");
+ _dialogService.ShowWarning("저장할 데이터가 없습니다.");
return;
}
@@ -106,7 +125,7 @@ namespace leak_test_project.ViewModels
{
if (CsvExporter.ExportToCsv(SearchResults, saveFileDialog.FileName))
{
- MessageBox.Show("CSV 파일로 저장되었습니다.");
+ _dialogService.ShowMessage("CSV 파일로 저장되었습니다.");
}
}
}
diff --git a/leak_test_project/ViewModels/HomeViewModel.cs b/leak_test_project/ViewModels/HomeViewModel.cs
index 98f9e14..a04d446 100644
--- a/leak_test_project/ViewModels/HomeViewModel.cs
+++ b/leak_test_project/ViewModels/HomeViewModel.cs
@@ -127,8 +127,11 @@ namespace leak_test_project.ViewModels
- public HomeViewModel(IDioBoard dioBoard)
+ private readonly IDialogService _dialogService;
+
+ public HomeViewModel(IDioBoard dioBoard, IDialogService dialogService = null)
{
+ _dialogService = dialogService ?? new DefaultDialogService();
_dioBoard = dioBoard;
_dispatcher = Dispatcher.CurrentDispatcher;
@@ -261,7 +264,7 @@ namespace leak_test_project.ViewModels
});
// 자동 시험 프로세스
- _testProcess = new TestProcessService(_dioBoard, _leftSensor, _rightSensor, _sentinelService);
+ _testProcess = new TestProcessService(_dioBoard, _leftSensor, _rightSensor, _sentinelService, App.LoginResult);
_testProcess.ProgressChanged += (s, e) => _dispatcher.Invoke(() =>
{
@@ -332,11 +335,7 @@ namespace leak_test_project.ViewModels
if (e.TestIndex == 0) LeftError = msg;
else RightError = msg;
- MessageBox.Show(
- "프로그램 스팩과 센서의 스팩이 서로 맞지 않습니다.",
- "SPEC 교차 검증 경고",
- MessageBoxButton.OK,
- MessageBoxImage.Warning);
+ _dialogService.ShowWarning("프로그램 스팩과 센서의 스팩이 서로 맞지 않습니다.", "SPEC 교차 검증 경고");
}
});
@@ -425,7 +424,6 @@ namespace leak_test_project.ViewModels
{
ConfigManager.ConfigChanged -= OnConfigChanged;
CleanupAll();
- _dioBoard?.Dispose();
}
}
}
diff --git a/leak_test_project/Views/DataView.xaml b/leak_test_project/Views/DataView.xaml
index 4deb431..e37d7d9 100644
--- a/leak_test_project/Views/DataView.xaml
+++ b/leak_test_project/Views/DataView.xaml
@@ -88,50 +88,86 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
@@ -143,13 +179,22 @@
CanUserSortColumns="True" CanUserReorderColumns="False"
BorderThickness="0" SelectionMode="Extended" SelectionChanged="DataGrid_SelectionChanged">
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
diff --git a/leak_test_project/Views/StartupLoginWindow.xaml b/leak_test_project/Views/StartupLoginWindow.xaml
index c5beb11..ce8ea93 100644
--- a/leak_test_project/Views/StartupLoginWindow.xaml
+++ b/leak_test_project/Views/StartupLoginWindow.xaml
@@ -397,7 +397,6 @@
Style="{StaticResource FieldLabelStyle}"/>
@@ -415,7 +414,6 @@
Style="{StaticResource FieldLabelStyle}"/>
@@ -433,7 +431,6 @@
Style="{StaticResource FieldLabelStyle}"/>
@@ -451,7 +448,6 @@
Style="{StaticResource FieldLabelStyle}"/>
@@ -480,7 +476,6 @@
Style="{StaticResource FieldLabelStyle}"/>
@@ -527,7 +522,6 @@
Style="{StaticResource FieldLabelStyle}"/>
@@ -544,7 +538,6 @@
Style="{StaticResource FieldLabelStyle}"/>
@@ -561,7 +554,6 @@
Style="{StaticResource FieldLabelStyle}"/>
diff --git a/leak_test_project/Views/StartupLoginWindow.xaml.cs b/leak_test_project/Views/StartupLoginWindow.xaml.cs
index 74e27f8..0a46bf3 100644
--- a/leak_test_project/Views/StartupLoginWindow.xaml.cs
+++ b/leak_test_project/Views/StartupLoginWindow.xaml.cs
@@ -328,7 +328,7 @@ namespace Housing.Login
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Housing",
- "LoginHistory.json");
+ "LoginHistory.xml");
}
}
}
diff --git a/leak_test_project/bin/Debug/Database.ini b/leak_test_project/bin/Debug/Database.ini
new file mode 100644
index 0000000..3491e9c
--- /dev/null
+++ b/leak_test_project/bin/Debug/Database.ini
@@ -0,0 +1,24 @@
+; =====================================================================
+; Mobi Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)
+; =====================================================================
+
+; [데이터베이스 접속 상세 설정]
+; Db_Server : 중앙 MSSQL 데이터베이스 서버의 IP 주소 또는 호스트명
+; Db_Name : 검사 데이터를 저장할 대상 데이터베이스 이름
+; Db_User : 데이터베이스 접속 계정 ID (예: sa)
+; Db_Password: 데이터베이스 접속 계정의 비밀번호
+Db_Server=192.168.0.20
+Db_Name=NE1aW_PT_SENSOR
+Db_User=sa
+Db_Password=sa1234
+
+
+[Login]
+; Procedure: 로그인 검증에 사용할 DB 저장 프로시저명입니다. (공란일 경우 기본 테이블 조회 쿼리 작동)
+Procedure=dbo.CheckOperator
+
+; ProcessName: 로그인 검증 시 전달할 공정 이름입니다.
+ProcessName=EOL
+
+; OfflinePreview: 오프라인 모드 활성화 여부 (true = DB 연결 없이 로그인 통과, false = 실제 DB 인증)
+OfflinePreview=false
diff --git a/leak_test_project/bin/Debug/app.publish/leak_test_project.exe b/leak_test_project/bin/Debug/app.publish/leak_test_project.exe
index 2a1915b..af9f670 100644
Binary files a/leak_test_project/bin/Debug/app.publish/leak_test_project.exe and b/leak_test_project/bin/Debug/app.publish/leak_test_project.exe differ
diff --git a/leak_test_project/bin/Debug/leak_test_project.application b/leak_test_project/bin/Debug/leak_test_project.application
index d63242f..eedb28f 100644
--- a/leak_test_project/bin/Debug/leak_test_project.application
+++ b/leak_test_project/bin/Debug/leak_test_project.application
@@ -14,7 +14,7 @@
- l3ZX9B+uam5XK1m0sy2nwxnA38smbnh5KmVt6jFffEY=
+ oBxYPjguQtv1EuRHadukWOmGPPqWPod5v+TZToduxNk=
diff --git a/leak_test_project/bin/Debug/leak_test_project.exe b/leak_test_project/bin/Debug/leak_test_project.exe
index 2a1915b..af9f670 100644
Binary files a/leak_test_project/bin/Debug/leak_test_project.exe and b/leak_test_project/bin/Debug/leak_test_project.exe differ
diff --git a/leak_test_project/bin/Debug/leak_test_project.exe.manifest b/leak_test_project/bin/Debug/leak_test_project.exe.manifest
index 5a19c22..ae8c6d0 100644
--- a/leak_test_project/bin/Debug/leak_test_project.exe.manifest
+++ b/leak_test_project/bin/Debug/leak_test_project.exe.manifest
@@ -42,14 +42,14 @@
-
+
- K7q6P0n/e7IBxUL2PiojluRdc1gfMK7T7k4cyvQZhss=
+ NEvi6EY1Ysg2m7d04Ix80K81U0DF+ZZKnv+9QRT3SMc=
diff --git a/leak_test_project/bin/Debug/leak_test_project.pdb b/leak_test_project/bin/Debug/leak_test_project.pdb
index 2ba9275..0378060 100644
Binary files a/leak_test_project/bin/Debug/leak_test_project.pdb and b/leak_test_project/bin/Debug/leak_test_project.pdb differ
diff --git a/leak_test_project/bin/Release/Database.ini b/leak_test_project/bin/Release/Database.ini
new file mode 100644
index 0000000..3491e9c
--- /dev/null
+++ b/leak_test_project/bin/Release/Database.ini
@@ -0,0 +1,24 @@
+; =====================================================================
+; Mobi Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)
+; =====================================================================
+
+; [데이터베이스 접속 상세 설정]
+; Db_Server : 중앙 MSSQL 데이터베이스 서버의 IP 주소 또는 호스트명
+; Db_Name : 검사 데이터를 저장할 대상 데이터베이스 이름
+; Db_User : 데이터베이스 접속 계정 ID (예: sa)
+; Db_Password: 데이터베이스 접속 계정의 비밀번호
+Db_Server=192.168.0.20
+Db_Name=NE1aW_PT_SENSOR
+Db_User=sa
+Db_Password=sa1234
+
+
+[Login]
+; Procedure: 로그인 검증에 사용할 DB 저장 프로시저명입니다. (공란일 경우 기본 테이블 조회 쿼리 작동)
+Procedure=dbo.CheckOperator
+
+; ProcessName: 로그인 검증 시 전달할 공정 이름입니다.
+ProcessName=EOL
+
+; OfflinePreview: 오프라인 모드 활성화 여부 (true = DB 연결 없이 로그인 통과, false = 실제 DB 인증)
+OfflinePreview=false
diff --git a/leak_test_project/bin/Release/Logs/2026-06-18_system.log b/leak_test_project/bin/Release/Logs/2026-06-18_system.log
new file mode 100644
index 0000000..5727672
--- /dev/null
+++ b/leak_test_project/bin/Release/Logs/2026-06-18_system.log
@@ -0,0 +1,2 @@
+[11:13:29.052] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o
+[11:13:30.394] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:owt28006727ea97c7801
diff --git a/leak_test_project/bin/Release/Logs/2026-06-25_system.log b/leak_test_project/bin/Release/Logs/2026-06-25_system.log
new file mode 100644
index 0000000..6dd299b
--- /dev/null
+++ b/leak_test_project/bin/Release/Logs/2026-06-25_system.log
@@ -0,0 +1,8 @@
+[10:17:36.444] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o
+[10:17:37.692] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:owt28006727ea97c7801
+[10:17:39.458] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o
+[10:17:40.707] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_002101:owt28006727ea97c7801
+[10:17:43.159] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o
+[10:17:44.414] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_002101:owt28006727ea97c7801
+[10:17:46.175] [ERROR] [Board4251] Failed to receive response after 3 retries: x00o
+[10:17:47.441] [ERROR] [Board4251] Failed to receive response after 3 retries: x00c_001101:owt28006727ea97c7801
diff --git a/leak_test_project/bin/Release/app.publish/leak_test_project.exe b/leak_test_project/bin/Release/app.publish/leak_test_project.exe
index 59820f7..88551aa 100644
Binary files a/leak_test_project/bin/Release/app.publish/leak_test_project.exe and b/leak_test_project/bin/Release/app.publish/leak_test_project.exe differ
diff --git a/leak_test_project/bin/Release/config.xml b/leak_test_project/bin/Release/config.xml
index 96e44e1..11e684a 100644
--- a/leak_test_project/bin/Release/config.xml
+++ b/leak_test_project/bin/Release/config.xml
@@ -6,6 +6,5 @@
115200
5000
0.1
- -0.2
- Server=192.168.0.20;Database=NE1aW_PT_SENSOR;User Id=sa;Password=sa1234;
+ -0.1
\ No newline at end of file
diff --git a/leak_test_project/bin/Release/leak_test_project.application b/leak_test_project/bin/Release/leak_test_project.application
index c6bb35e..2b30ded 100644
--- a/leak_test_project/bin/Release/leak_test_project.application
+++ b/leak_test_project/bin/Release/leak_test_project.application
@@ -14,7 +14,7 @@
- qNbiAc1kf0eiU+DGDJlSHaQrr7D5MVNH8vmLeW563VI=
+ n7VmvcxpOf8Y+LRFjH5TmdoI+vXXgYov1j6fqW/Nbxs=
diff --git a/leak_test_project/bin/Release/leak_test_project.exe b/leak_test_project/bin/Release/leak_test_project.exe
index 59820f7..88551aa 100644
Binary files a/leak_test_project/bin/Release/leak_test_project.exe and b/leak_test_project/bin/Release/leak_test_project.exe differ
diff --git a/leak_test_project/bin/Release/leak_test_project.exe.manifest b/leak_test_project/bin/Release/leak_test_project.exe.manifest
index 99ec36a..8933370 100644
--- a/leak_test_project/bin/Release/leak_test_project.exe.manifest
+++ b/leak_test_project/bin/Release/leak_test_project.exe.manifest
@@ -42,14 +42,14 @@
-
+
- oU1Vb+qDbIJPdoRRPncFp5ECBdIWWwNNrCazZR4lxdI=
+ bIiBUDqHxOgJSZaDgyYguxKJoGO3oFxaqRp+FVa4a/A=
diff --git a/leak_test_project/bin/Release/leak_test_project.pdb b/leak_test_project/bin/Release/leak_test_project.pdb
index ae41983..ab63dcb 100644
Binary files a/leak_test_project/bin/Release/leak_test_project.pdb and b/leak_test_project/bin/Release/leak_test_project.pdb differ
diff --git a/leak_test_project/leak_test_project.csproj b/leak_test_project/leak_test_project.csproj
index 40a4b4f..0201ac5 100644
--- a/leak_test_project/leak_test_project.csproj
+++ b/leak_test_project/leak_test_project.csproj
@@ -94,7 +94,6 @@
Designer
MSBuild:Compile
-
Designer
MSBuild:Compile
@@ -129,15 +128,18 @@
-
+
+
+
+
+
HomeView.xaml
-
DataView.xaml
@@ -154,7 +156,6 @@
-
@@ -189,6 +190,9 @@
+
+ PreserveNewest
+
diff --git a/leak_test_project/obj/Debug/App.baml b/leak_test_project/obj/Debug/App.baml
index 6879520..42763a7 100644
Binary files a/leak_test_project/obj/Debug/App.baml and b/leak_test_project/obj/Debug/App.baml differ
diff --git a/leak_test_project/obj/Debug/MainWindow.baml b/leak_test_project/obj/Debug/MainWindow.baml
index 5f75708..efd5d2e 100644
Binary files a/leak_test_project/obj/Debug/MainWindow.baml and b/leak_test_project/obj/Debug/MainWindow.baml differ
diff --git a/leak_test_project/obj/Debug/Views/CommunicationWindow.baml b/leak_test_project/obj/Debug/Views/CommunicationWindow.baml
index 4dedbb5..526142d 100644
Binary files a/leak_test_project/obj/Debug/Views/CommunicationWindow.baml and b/leak_test_project/obj/Debug/Views/CommunicationWindow.baml differ
diff --git a/leak_test_project/obj/Debug/Views/DataView.baml b/leak_test_project/obj/Debug/Views/DataView.baml
index c7f8fc2..009d8a4 100644
Binary files a/leak_test_project/obj/Debug/Views/DataView.baml and b/leak_test_project/obj/Debug/Views/DataView.baml differ
diff --git a/leak_test_project/obj/Debug/Views/DataView.g.cs b/leak_test_project/obj/Debug/Views/DataView.g.cs
index ecec15f..f52a556 100644
--- a/leak_test_project/obj/Debug/Views/DataView.g.cs
+++ b/leak_test_project/obj/Debug/Views/DataView.g.cs
@@ -1,4 +1,4 @@
-#pragma checksum "..\..\..\Views\DataView.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "CF3D9FA0B3A93AD2D6C065F35DF39EBFC783D422DB5170F1B6A1280CD7D48859"
+#pragma checksum "..\..\..\Views\DataView.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "E806D581E87844448860858BA46A605448A6BF6EEC5FF26FF0D442F616E4D3C7"
//------------------------------------------------------------------------------
//
// 이 코드는 도구를 사용하여 생성되었습니다.
@@ -71,7 +71,7 @@ namespace leak_test_project.Views {
{
case 1:
- #line 134 "..\..\..\Views\DataView.xaml"
+ #line 169 "..\..\..\Views\DataView.xaml"
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Button_Click);
#line default
@@ -79,7 +79,7 @@ namespace leak_test_project.Views {
return;
case 2:
- #line 144 "..\..\..\Views\DataView.xaml"
+ #line 180 "..\..\..\Views\DataView.xaml"
((System.Windows.Controls.DataGrid)(target)).SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.DataGrid_SelectionChanged);
#line default
diff --git a/leak_test_project/obj/Debug/Views/HomeView.baml b/leak_test_project/obj/Debug/Views/HomeView.baml
index 5ebc07d..f9a5797 100644
Binary files a/leak_test_project/obj/Debug/Views/HomeView.baml and b/leak_test_project/obj/Debug/Views/HomeView.baml differ
diff --git a/leak_test_project/obj/Debug/Views/ParametersWindow.baml b/leak_test_project/obj/Debug/Views/ParametersWindow.baml
index 17279ad..b27dce5 100644
Binary files a/leak_test_project/obj/Debug/Views/ParametersWindow.baml and b/leak_test_project/obj/Debug/Views/ParametersWindow.baml differ
diff --git a/leak_test_project/obj/Debug/Views/StartupLoginWindow.baml b/leak_test_project/obj/Debug/Views/StartupLoginWindow.baml
index bcdedd1..c03d2a7 100644
Binary files a/leak_test_project/obj/Debug/Views/StartupLoginWindow.baml and b/leak_test_project/obj/Debug/Views/StartupLoginWindow.baml differ
diff --git a/leak_test_project/obj/Debug/Views/StartupLoginWindow.g.cs b/leak_test_project/obj/Debug/Views/StartupLoginWindow.g.cs
index 8c521ae..7399558 100644
--- a/leak_test_project/obj/Debug/Views/StartupLoginWindow.g.cs
+++ b/leak_test_project/obj/Debug/Views/StartupLoginWindow.g.cs
@@ -1,4 +1,4 @@
-#pragma checksum "..\..\..\Views\StartupLoginWindow.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "10236E62B98A3B25C61EAC333EF6CCD078A69767002979415693621924B5E4AC"
+#pragma checksum "..\..\..\Views\StartupLoginWindow.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "E10917AAAC5C59A6940A4937C1165BBFED3D9CE02854042592965F3FCDF2EE1F"
//------------------------------------------------------------------------------
//
// 이 코드는 도구를 사용하여 생성되었습니다.
@@ -48,7 +48,7 @@ namespace Housing.Login {
#line hidden
- #line 416 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 415 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox ModelTextBox;
@@ -56,7 +56,7 @@ namespace Housing.Login {
#line hidden
- #line 434 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 432 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox Variant1TextBox;
@@ -64,7 +64,7 @@ namespace Housing.Login {
#line hidden
- #line 452 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 449 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox Variant2TextBox;
@@ -72,7 +72,7 @@ namespace Housing.Login {
#line hidden
- #line 481 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 477 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox OperatorTextBox;
@@ -80,7 +80,7 @@ namespace Housing.Login {
#line hidden
- #line 498 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 493 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.PasswordBox PasswordBox;
@@ -88,7 +88,7 @@ namespace Housing.Login {
#line hidden
- #line 528 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 523 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox LineNoTextBox;
@@ -96,7 +96,7 @@ namespace Housing.Login {
#line hidden
- #line 545 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 539 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox LotNoTextBox;
@@ -104,7 +104,7 @@ namespace Housing.Login {
#line hidden
- #line 562 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 555 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox JigNoTextBox;
@@ -112,7 +112,7 @@ namespace Housing.Login {
#line hidden
- #line 570 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 562 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button LoginButton;
@@ -179,7 +179,7 @@ namespace Housing.Login {
case 10:
this.LoginButton = ((System.Windows.Controls.Button)(target));
- #line 574 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 566 "..\..\..\Views\StartupLoginWindow.xaml"
this.LoginButton.Click += new System.Windows.RoutedEventHandler(this.LogInButton_Click);
#line default
diff --git a/leak_test_project/obj/Debug/leak_test_project.application b/leak_test_project/obj/Debug/leak_test_project.application
index d63242f..eedb28f 100644
--- a/leak_test_project/obj/Debug/leak_test_project.application
+++ b/leak_test_project/obj/Debug/leak_test_project.application
@@ -14,7 +14,7 @@
- l3ZX9B+uam5XK1m0sy2nwxnA38smbnh5KmVt6jFffEY=
+ oBxYPjguQtv1EuRHadukWOmGPPqWPod5v+TZToduxNk=
diff --git a/leak_test_project/obj/Debug/leak_test_project.csproj.CoreCompileInputs.cache b/leak_test_project/obj/Debug/leak_test_project.csproj.CoreCompileInputs.cache
index 3c27733..4cae234 100644
--- a/leak_test_project/obj/Debug/leak_test_project.csproj.CoreCompileInputs.cache
+++ b/leak_test_project/obj/Debug/leak_test_project.csproj.CoreCompileInputs.cache
@@ -1 +1 @@
-f74477eeb98a106556f1b65e440f8cfa8362ac43ae60c03174d4f2bb878b7a02
+628713ffbc5f55cd805898b1d31f7980abb2a5a8c3c1aad9e2f31b3c27ae4897
diff --git a/leak_test_project/obj/Debug/leak_test_project.csproj.FileListAbsolute.txt b/leak_test_project/obj/Debug/leak_test_project.csproj.FileListAbsolute.txt
index 6e95583..dd8c72a 100644
--- a/leak_test_project/obj/Debug/leak_test_project.csproj.FileListAbsolute.txt
+++ b/leak_test_project/obj/Debug/leak_test_project.csproj.FileListAbsolute.txt
@@ -30,3 +30,4 @@ C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Debug\le
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Debug\leak_test_project.application
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Debug\leak_test_project.exe
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Debug\leak_test_project.pdb
+C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\bin\Debug\Database.ini
diff --git a/leak_test_project/obj/Debug/leak_test_project.exe b/leak_test_project/obj/Debug/leak_test_project.exe
index 2a1915b..af9f670 100644
Binary files a/leak_test_project/obj/Debug/leak_test_project.exe and b/leak_test_project/obj/Debug/leak_test_project.exe differ
diff --git a/leak_test_project/obj/Debug/leak_test_project.exe.manifest b/leak_test_project/obj/Debug/leak_test_project.exe.manifest
index 5a19c22..ae8c6d0 100644
--- a/leak_test_project/obj/Debug/leak_test_project.exe.manifest
+++ b/leak_test_project/obj/Debug/leak_test_project.exe.manifest
@@ -42,14 +42,14 @@
-
+
- K7q6P0n/e7IBxUL2PiojluRdc1gfMK7T7k4cyvQZhss=
+ NEvi6EY1Ysg2m7d04Ix80K81U0DF+ZZKnv+9QRT3SMc=
diff --git a/leak_test_project/obj/Debug/leak_test_project.g.resources b/leak_test_project/obj/Debug/leak_test_project.g.resources
index ca2bb07..2879138 100644
Binary files a/leak_test_project/obj/Debug/leak_test_project.g.resources and b/leak_test_project/obj/Debug/leak_test_project.g.resources differ
diff --git a/leak_test_project/obj/Debug/leak_test_project.pdb b/leak_test_project/obj/Debug/leak_test_project.pdb
index 2ba9275..0378060 100644
Binary files a/leak_test_project/obj/Debug/leak_test_project.pdb and b/leak_test_project/obj/Debug/leak_test_project.pdb differ
diff --git a/leak_test_project/obj/Debug/leak_test_project_MarkupCompile.cache b/leak_test_project/obj/Debug/leak_test_project_MarkupCompile.cache
index c410727..bd78dc0 100644
--- a/leak_test_project/obj/Debug/leak_test_project_MarkupCompile.cache
+++ b/leak_test_project/obj/Debug/leak_test_project_MarkupCompile.cache
@@ -10,10 +10,10 @@ none
false
DEBUG;TRACE
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\App.xaml
-6681427662
-1-698605034
-311518714309
-14-767506772
+6-1954804774
+1-1942997596
+35231504871
+14188644636
Views\HomeView.xaml;Views\DataView.xaml;Views\ParametersWindow.xaml;Views\CommunicationWindow.xaml;Views\StartupLoginWindow.xaml;MainWindow.xaml;
False
diff --git a/leak_test_project/obj/Release/App.baml b/leak_test_project/obj/Release/App.baml
index 29b1621..ebfb366 100644
Binary files a/leak_test_project/obj/Release/App.baml and b/leak_test_project/obj/Release/App.baml differ
diff --git a/leak_test_project/obj/Release/DesignTimeResolveAssemblyReferencesInput.cache b/leak_test_project/obj/Release/DesignTimeResolveAssemblyReferencesInput.cache
index c8d80e2..ae5ad75 100644
Binary files a/leak_test_project/obj/Release/DesignTimeResolveAssemblyReferencesInput.cache and b/leak_test_project/obj/Release/DesignTimeResolveAssemblyReferencesInput.cache differ
diff --git a/leak_test_project/obj/Release/GeneratedInternalTypeHelper.g.cs b/leak_test_project/obj/Release/GeneratedInternalTypeHelper.g.cs
index 04dc0b9..c65238f 100644
--- a/leak_test_project/obj/Release/GeneratedInternalTypeHelper.g.cs
+++ b/leak_test_project/obj/Release/GeneratedInternalTypeHelper.g.cs
@@ -1,62 +1,2 @@
-//------------------------------------------------------------------------------
-//
-// 이 코드는 도구를 사용하여 생성되었습니다.
-// 런타임 버전:4.0.30319.42000
-//
-// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
-// 이러한 변경 내용이 손실됩니다.
-//
-//------------------------------------------------------------------------------
-
-namespace XamlGeneratedNamespace {
-
-
- ///
- /// GeneratedInternalTypeHelper
- ///
- [System.Diagnostics.DebuggerNonUserCodeAttribute()]
- [System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]
- [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
- public sealed class GeneratedInternalTypeHelper : System.Windows.Markup.InternalTypeHelper {
-
- ///
- /// CreateInstance
- ///
- protected override object CreateInstance(System.Type type, System.Globalization.CultureInfo culture) {
- return System.Activator.CreateInstance(type, ((System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)
- | (System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.CreateInstance)), null, null, culture);
- }
-
- ///
- /// GetPropertyValue
- ///
- protected override object GetPropertyValue(System.Reflection.PropertyInfo propertyInfo, object target, System.Globalization.CultureInfo culture) {
- return propertyInfo.GetValue(target, System.Reflection.BindingFlags.Default, null, null, culture);
- }
-
- ///
- /// SetPropertyValue
- ///
- protected override void SetPropertyValue(System.Reflection.PropertyInfo propertyInfo, object target, object value, System.Globalization.CultureInfo culture) {
- propertyInfo.SetValue(target, value, System.Reflection.BindingFlags.Default, null, null, culture);
- }
-
- ///
- /// CreateDelegate
- ///
- protected override System.Delegate CreateDelegate(System.Type delegateType, object target, string handler) {
- return ((System.Delegate)(target.GetType().InvokeMember("_CreateDelegate", (System.Reflection.BindingFlags.InvokeMethod
- | (System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)), null, target, new object[] {
- delegateType,
- handler}, null)));
- }
-
- ///
- /// AddEventHandler
- ///
- protected override void AddEventHandler(System.Reflection.EventInfo eventInfo, object target, System.Delegate handler) {
- eventInfo.AddEventHandler(target, handler);
- }
- }
-}
+
diff --git a/leak_test_project/obj/Release/MainWindow.baml b/leak_test_project/obj/Release/MainWindow.baml
index 4b580aa..c5973ba 100644
Binary files a/leak_test_project/obj/Release/MainWindow.baml and b/leak_test_project/obj/Release/MainWindow.baml differ
diff --git a/leak_test_project/obj/Release/Views/CommunicationWindow.baml b/leak_test_project/obj/Release/Views/CommunicationWindow.baml
index e86c526..10d93f8 100644
Binary files a/leak_test_project/obj/Release/Views/CommunicationWindow.baml and b/leak_test_project/obj/Release/Views/CommunicationWindow.baml differ
diff --git a/leak_test_project/obj/Release/Views/DataView.baml b/leak_test_project/obj/Release/Views/DataView.baml
index 375aa97..36db689 100644
Binary files a/leak_test_project/obj/Release/Views/DataView.baml and b/leak_test_project/obj/Release/Views/DataView.baml differ
diff --git a/leak_test_project/obj/Release/Views/DataView.g.cs b/leak_test_project/obj/Release/Views/DataView.g.cs
index ecec15f..f52a556 100644
--- a/leak_test_project/obj/Release/Views/DataView.g.cs
+++ b/leak_test_project/obj/Release/Views/DataView.g.cs
@@ -1,4 +1,4 @@
-#pragma checksum "..\..\..\Views\DataView.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "CF3D9FA0B3A93AD2D6C065F35DF39EBFC783D422DB5170F1B6A1280CD7D48859"
+#pragma checksum "..\..\..\Views\DataView.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "E806D581E87844448860858BA46A605448A6BF6EEC5FF26FF0D442F616E4D3C7"
//------------------------------------------------------------------------------
//
// 이 코드는 도구를 사용하여 생성되었습니다.
@@ -71,7 +71,7 @@ namespace leak_test_project.Views {
{
case 1:
- #line 134 "..\..\..\Views\DataView.xaml"
+ #line 169 "..\..\..\Views\DataView.xaml"
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Button_Click);
#line default
@@ -79,7 +79,7 @@ namespace leak_test_project.Views {
return;
case 2:
- #line 144 "..\..\..\Views\DataView.xaml"
+ #line 180 "..\..\..\Views\DataView.xaml"
((System.Windows.Controls.DataGrid)(target)).SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.DataGrid_SelectionChanged);
#line default
diff --git a/leak_test_project/obj/Release/Views/DataView.g.i.cs b/leak_test_project/obj/Release/Views/DataView.g.i.cs
index ecec15f..f52a556 100644
--- a/leak_test_project/obj/Release/Views/DataView.g.i.cs
+++ b/leak_test_project/obj/Release/Views/DataView.g.i.cs
@@ -1,4 +1,4 @@
-#pragma checksum "..\..\..\Views\DataView.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "CF3D9FA0B3A93AD2D6C065F35DF39EBFC783D422DB5170F1B6A1280CD7D48859"
+#pragma checksum "..\..\..\Views\DataView.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "E806D581E87844448860858BA46A605448A6BF6EEC5FF26FF0D442F616E4D3C7"
//------------------------------------------------------------------------------
//
// 이 코드는 도구를 사용하여 생성되었습니다.
@@ -71,7 +71,7 @@ namespace leak_test_project.Views {
{
case 1:
- #line 134 "..\..\..\Views\DataView.xaml"
+ #line 169 "..\..\..\Views\DataView.xaml"
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Button_Click);
#line default
@@ -79,7 +79,7 @@ namespace leak_test_project.Views {
return;
case 2:
- #line 144 "..\..\..\Views\DataView.xaml"
+ #line 180 "..\..\..\Views\DataView.xaml"
((System.Windows.Controls.DataGrid)(target)).SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.DataGrid_SelectionChanged);
#line default
diff --git a/leak_test_project/obj/Release/Views/HomeView.baml b/leak_test_project/obj/Release/Views/HomeView.baml
index b3cf85c..dd36aea 100644
Binary files a/leak_test_project/obj/Release/Views/HomeView.baml and b/leak_test_project/obj/Release/Views/HomeView.baml differ
diff --git a/leak_test_project/obj/Release/Views/ParametersWindow.baml b/leak_test_project/obj/Release/Views/ParametersWindow.baml
index 14564bf..89ad05e 100644
Binary files a/leak_test_project/obj/Release/Views/ParametersWindow.baml and b/leak_test_project/obj/Release/Views/ParametersWindow.baml differ
diff --git a/leak_test_project/obj/Release/Views/StartupLoginWindow.baml b/leak_test_project/obj/Release/Views/StartupLoginWindow.baml
index 0a1dfaf..83f70fe 100644
Binary files a/leak_test_project/obj/Release/Views/StartupLoginWindow.baml and b/leak_test_project/obj/Release/Views/StartupLoginWindow.baml differ
diff --git a/leak_test_project/obj/Release/Views/StartupLoginWindow.g.cs b/leak_test_project/obj/Release/Views/StartupLoginWindow.g.cs
index 8c521ae..7399558 100644
--- a/leak_test_project/obj/Release/Views/StartupLoginWindow.g.cs
+++ b/leak_test_project/obj/Release/Views/StartupLoginWindow.g.cs
@@ -1,4 +1,4 @@
-#pragma checksum "..\..\..\Views\StartupLoginWindow.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "10236E62B98A3B25C61EAC333EF6CCD078A69767002979415693621924B5E4AC"
+#pragma checksum "..\..\..\Views\StartupLoginWindow.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "E10917AAAC5C59A6940A4937C1165BBFED3D9CE02854042592965F3FCDF2EE1F"
//------------------------------------------------------------------------------
//
// 이 코드는 도구를 사용하여 생성되었습니다.
@@ -48,7 +48,7 @@ namespace Housing.Login {
#line hidden
- #line 416 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 415 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox ModelTextBox;
@@ -56,7 +56,7 @@ namespace Housing.Login {
#line hidden
- #line 434 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 432 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox Variant1TextBox;
@@ -64,7 +64,7 @@ namespace Housing.Login {
#line hidden
- #line 452 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 449 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox Variant2TextBox;
@@ -72,7 +72,7 @@ namespace Housing.Login {
#line hidden
- #line 481 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 477 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox OperatorTextBox;
@@ -80,7 +80,7 @@ namespace Housing.Login {
#line hidden
- #line 498 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 493 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.PasswordBox PasswordBox;
@@ -88,7 +88,7 @@ namespace Housing.Login {
#line hidden
- #line 528 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 523 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox LineNoTextBox;
@@ -96,7 +96,7 @@ namespace Housing.Login {
#line hidden
- #line 545 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 539 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox LotNoTextBox;
@@ -104,7 +104,7 @@ namespace Housing.Login {
#line hidden
- #line 562 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 555 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox JigNoTextBox;
@@ -112,7 +112,7 @@ namespace Housing.Login {
#line hidden
- #line 570 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 562 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button LoginButton;
@@ -179,7 +179,7 @@ namespace Housing.Login {
case 10:
this.LoginButton = ((System.Windows.Controls.Button)(target));
- #line 574 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 566 "..\..\..\Views\StartupLoginWindow.xaml"
this.LoginButton.Click += new System.Windows.RoutedEventHandler(this.LogInButton_Click);
#line default
diff --git a/leak_test_project/obj/Release/Views/StartupLoginWindow.g.i.cs b/leak_test_project/obj/Release/Views/StartupLoginWindow.g.i.cs
index 8c521ae..7399558 100644
--- a/leak_test_project/obj/Release/Views/StartupLoginWindow.g.i.cs
+++ b/leak_test_project/obj/Release/Views/StartupLoginWindow.g.i.cs
@@ -1,4 +1,4 @@
-#pragma checksum "..\..\..\Views\StartupLoginWindow.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "10236E62B98A3B25C61EAC333EF6CCD078A69767002979415693621924B5E4AC"
+#pragma checksum "..\..\..\Views\StartupLoginWindow.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "E10917AAAC5C59A6940A4937C1165BBFED3D9CE02854042592965F3FCDF2EE1F"
//------------------------------------------------------------------------------
//
// 이 코드는 도구를 사용하여 생성되었습니다.
@@ -48,7 +48,7 @@ namespace Housing.Login {
#line hidden
- #line 416 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 415 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox ModelTextBox;
@@ -56,7 +56,7 @@ namespace Housing.Login {
#line hidden
- #line 434 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 432 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox Variant1TextBox;
@@ -64,7 +64,7 @@ namespace Housing.Login {
#line hidden
- #line 452 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 449 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox Variant2TextBox;
@@ -72,7 +72,7 @@ namespace Housing.Login {
#line hidden
- #line 481 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 477 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox OperatorTextBox;
@@ -80,7 +80,7 @@ namespace Housing.Login {
#line hidden
- #line 498 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 493 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.PasswordBox PasswordBox;
@@ -88,7 +88,7 @@ namespace Housing.Login {
#line hidden
- #line 528 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 523 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox LineNoTextBox;
@@ -96,7 +96,7 @@ namespace Housing.Login {
#line hidden
- #line 545 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 539 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox LotNoTextBox;
@@ -104,7 +104,7 @@ namespace Housing.Login {
#line hidden
- #line 562 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 555 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.TextBox JigNoTextBox;
@@ -112,7 +112,7 @@ namespace Housing.Login {
#line hidden
- #line 570 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 562 "..\..\..\Views\StartupLoginWindow.xaml"
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
internal System.Windows.Controls.Button LoginButton;
@@ -179,7 +179,7 @@ namespace Housing.Login {
case 10:
this.LoginButton = ((System.Windows.Controls.Button)(target));
- #line 574 "..\..\..\Views\StartupLoginWindow.xaml"
+ #line 566 "..\..\..\Views\StartupLoginWindow.xaml"
this.LoginButton.Click += new System.Windows.RoutedEventHandler(this.LogInButton_Click);
#line default
diff --git a/leak_test_project/obj/Release/leak_test_project.application b/leak_test_project/obj/Release/leak_test_project.application
index c6bb35e..2b30ded 100644
--- a/leak_test_project/obj/Release/leak_test_project.application
+++ b/leak_test_project/obj/Release/leak_test_project.application
@@ -14,7 +14,7 @@
- qNbiAc1kf0eiU+DGDJlSHaQrr7D5MVNH8vmLeW563VI=
+ n7VmvcxpOf8Y+LRFjH5TmdoI+vXXgYov1j6fqW/Nbxs=
diff --git a/leak_test_project/obj/Release/leak_test_project.csproj.AssemblyReference.cache b/leak_test_project/obj/Release/leak_test_project.csproj.AssemblyReference.cache
index 069e925..e40b0fa 100644
Binary files a/leak_test_project/obj/Release/leak_test_project.csproj.AssemblyReference.cache and b/leak_test_project/obj/Release/leak_test_project.csproj.AssemblyReference.cache differ
diff --git a/leak_test_project/obj/Release/leak_test_project.csproj.CoreCompileInputs.cache b/leak_test_project/obj/Release/leak_test_project.csproj.CoreCompileInputs.cache
index b001bcd..25ead8a 100644
--- a/leak_test_project/obj/Release/leak_test_project.csproj.CoreCompileInputs.cache
+++ b/leak_test_project/obj/Release/leak_test_project.csproj.CoreCompileInputs.cache
@@ -1 +1 @@
-cbc4c65e0c5c8abdc171ee59e1f11d54d6b7bd62192c87410037a9efab12cf46
+8765edc9fc5fcfe1db8d41cec7744abfac5abd652e5521541b6bb33dc8fa7af8
diff --git a/leak_test_project/obj/Release/leak_test_project.csproj.FileListAbsolute.txt b/leak_test_project/obj/Release/leak_test_project.csproj.FileListAbsolute.txt
index 4d0e880..a2e27de 100644
--- a/leak_test_project/obj/Release/leak_test_project.csproj.FileListAbsolute.txt
+++ b/leak_test_project/obj/Release/leak_test_project.csproj.FileListAbsolute.txt
@@ -30,3 +30,4 @@ C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Release\
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Release\leak_test_project.pdb
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Release\Views\StartupLoginWindow.baml
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Release\Views\StartupLoginWindow.g.cs
+C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\bin\Release\Database.ini
diff --git a/leak_test_project/obj/Release/leak_test_project.exe b/leak_test_project/obj/Release/leak_test_project.exe
index 59820f7..88551aa 100644
Binary files a/leak_test_project/obj/Release/leak_test_project.exe and b/leak_test_project/obj/Release/leak_test_project.exe differ
diff --git a/leak_test_project/obj/Release/leak_test_project.exe.manifest b/leak_test_project/obj/Release/leak_test_project.exe.manifest
index 99ec36a..8933370 100644
--- a/leak_test_project/obj/Release/leak_test_project.exe.manifest
+++ b/leak_test_project/obj/Release/leak_test_project.exe.manifest
@@ -42,14 +42,14 @@
-
+
- oU1Vb+qDbIJPdoRRPncFp5ECBdIWWwNNrCazZR4lxdI=
+ bIiBUDqHxOgJSZaDgyYguxKJoGO3oFxaqRp+FVa4a/A=
diff --git a/leak_test_project/obj/Release/leak_test_project.g.resources b/leak_test_project/obj/Release/leak_test_project.g.resources
index 1278fe8..7c6c13a 100644
Binary files a/leak_test_project/obj/Release/leak_test_project.g.resources and b/leak_test_project/obj/Release/leak_test_project.g.resources differ
diff --git a/leak_test_project/obj/Release/leak_test_project.pdb b/leak_test_project/obj/Release/leak_test_project.pdb
index ae41983..ab63dcb 100644
Binary files a/leak_test_project/obj/Release/leak_test_project.pdb and b/leak_test_project/obj/Release/leak_test_project.pdb differ
diff --git a/leak_test_project/obj/Release/leak_test_project_MarkupCompile.cache b/leak_test_project/obj/Release/leak_test_project_MarkupCompile.cache
index 978393d..62ddc40 100644
--- a/leak_test_project/obj/Release/leak_test_project_MarkupCompile.cache
+++ b/leak_test_project/obj/Release/leak_test_project_MarkupCompile.cache
@@ -10,10 +10,10 @@ none
false
TRACE
C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\App.xaml
-6681427662
-1-698605034
-311518714309
-14-767506772
+6-1954804774
+1-1942997596
+35231504871
+14188644636
Views\HomeView.xaml;Views\DataView.xaml;Views\ParametersWindow.xaml;Views\CommunicationWindow.xaml;Views\StartupLoginWindow.xaml;MainWindow.xaml;
False
diff --git a/leak_test_project/obj/Release/leak_test_project_MarkupCompile.lref b/leak_test_project/obj/Release/leak_test_project_MarkupCompile.lref
index ed3694c..bef503a 100644
--- a/leak_test_project/obj/Release/leak_test_project_MarkupCompile.lref
+++ b/leak_test_project/obj/Release/leak_test_project_MarkupCompile.lref
@@ -1,4 +1,4 @@
-
+C:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\obj\Release\GeneratedInternalTypeHelper.g.cs
FC:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\App.xaml;;
FC:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\Views\HomeView.xaml;;
FC:\Users\COMPUTER1\Desktop\mobi\leak_test_project\leak_test_project\MainWindow.xaml;;
diff --git a/ob/01_System_Architecture.md b/ob/01_System_Architecture.md
new file mode 100644
index 0000000..dc0e4a9
--- /dev/null
+++ b/ob/01_System_Architecture.md
@@ -0,0 +1,135 @@
+# [1단계] 아키텍처 및 기술 스택 정의 (System Architecture)
+
+본 문서는 누수 시험 장비 제어 및 모니터링 시스템(WPF Leak Test Project)의 전체적인 시스템 구조, 데이터 흐름, 그리고 사용되는 기술 스택을 정의합니다.
+
+---
+
+## 1. 기술 스택 정의 및 채택 이유 (Technology Stack)
+
+본 시스템은 공장 생산 라인(EOL)의 실시간 누수 테스트 장비 제어 및 신뢰성 데이터 수집을 목적으로 하므로, 장비 제어의 실시간성, Windows OS 환경에서의 하드웨어 통신 드라이버 호환성, 폐쇄망 환경의 안정적인 데이터 보관을 고려하여 기술 스택을 정의합니다.
+
+| 분류 | 기술 스택 | 채택 이유 |
+| :--- | :--- | :--- |
+| **클라이언트 (데스크톱 제어 앱)** | **WPF (.NET Framework 4.7.2 / C#)** | Windows 기반 하드웨어 및 계측 장비 제어 API(DASK, Serial) 호환성 확보. MVVM 패턴을 통한 데이터 바인딩 및 시각 효과 구현 극대화. |
+| **데이터베이스 (DB)** | **Microsoft SQL Server (MSSQL)** | 현장 계측 데이터의 트랜잭션 보장 및 공장 내 타 ERP/MES 시스템과의 데이터 호환 용이성. UPSERT(MERGE) 구문을 통한 중복 데이터 방지. |
+| **하드웨어 인터페이스** | **RS-232 Serial (COM Port) / PCI DASK API** | Sentinel C28 누수 측정기 및 ADLINK 4251 컨트롤러 보드와의 직접 제어 프로토콜 통신. |
+| **로그 시스템 (로컬 이중화)** | **CSV / Raw Text File System** | 네트워크 단절(DB 오프라인) 상황에서도 계측 데이터 유실을 방지하기 위한 로컬 디스크 이중 백업. |
+
+---
+
+## 2. 시스템 아키텍처 구조도 (System Architecture Diagram)
+
+생산 현장에 위치한 제어 PC 내부의 컴포넌트 구조와 외부 물리 장비 및 데이터베이스 간의 연결 형태는 다음과 같습니다.
+
+```mermaid
+graph TD
+ %% 외부 물리 디바이스 영역
+ subgraph HW ["Physical Hardware Devices"]
+ C28["Sentinel C28 (Leak Sensor)"]
+ DioBoard["ADLINK 4251 Board (DIO & ID Reader)"]
+ end
+
+ %% WPF 클라이언트 영역
+ subgraph Client ["WPF Desktop Application (Client)"]
+ %% UI Layer
+ subgraph ViewLayer ["UI (View) Layer"]
+ HomeV["Home View (Real-time Test)"]
+ DataV["Data View (Query & CSV Export)"]
+ LoginW["Startup Login Window"]
+ end
+
+ %% ViewModel Layer
+ subgraph VMLayer ["ViewModel Layer"]
+ HomeVM["Home ViewModel"]
+ DataVM["Data ViewModel"]
+ end
+
+ %% Service Layer
+ subgraph ServiceLayer ["Business / Service Layer"]
+ TPS["TestProcessService (Test Loop Control)"]
+ B4251["Board4251Service (ID Reader & IO)"]
+ C28S["SentinelC28Service (C28 Protocol)"]
+ DBS["EolDatabaseService (DB Adapter)"]
+ end
+
+ %% Utils / Models
+ subgraph Common ["Utils & Models"]
+ FL["FileLogger (Local CSV Backup)"]
+ DS["IDialogService (Message Popups)"]
+ Config["ConfigManager (Settings)"]
+ end
+ end
+
+ %% 영속화 스토리지 영역
+ subgraph Storage ["Persistence Storage Layer"]
+ DB[("MSSQL Database Server\n[dbo].[EOL]")]
+ Disk[("Local Disk\nLogs/yyyy-MM-dd.csv")]
+ end
+
+ %% 연결 관계 정의
+ C28 <-->|RS-232 Serial| C28S
+ DioBoard <-->|RS-232 / PCI Dask| B4251
+
+ C28S -->|Parsed Stream Data| HomeVM
+ B4251 -->|ID / IO State| TPS
+ C28S -->|Final Result Event| TPS
+ TPS -->|Test Progress & Result| HomeVM
+
+ HomeVM -->|Data Binding| HomeV
+ DataVM -->|Data Binding| DataV
+
+ TPS -->|Save Command| DBS
+ TPS -->|Backup Command| FL
+
+ DBS -->|ADO.NET SQL Write| DB
+ FL -->|File Append| Disk
+ DataVM -->|Read DB / Fallback CSV| Disk
+ DataVM -.->|Read DB| DB
+```
+
+---
+
+## 3. 데이터 흐름 (Data Flow)
+
+시험 시작부터 DB 및 로컬 저장까지의 계측 데이터 흐름은 다음 단계를 따릅니다.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ actor Operator as 작업자
+ participant HW_IO as ADLINK 4251 (DIO)
+ participant TPS as TestProcessService (WPF Thread)
+ participant Sensor as ID Sensor (Board4251)
+ participant C28 as Sentinel C28 (Leak Sensor)
+ participant DBS as EolDatabaseService
+ participant LocalFile as FileLogger (Local Disk)
+ participant DB as MSSQL Database
+
+ Operator->>HW_IO: 물리 기동 스위치 조작 (START 신호)
+ HW_IO->>TPS: InputChanged 이벤트 발생 (LEFT_START / RIGHT_START)
+ Note over TPS: AutoResetEvent.Set() 으로 대기 스레드 기상
+ TPS->>Sensor: ID 판독 명령 (x00c_... 송신)
+ Sensor-->>TPS: 16자리 ID 문자열 반환 (제품 일련번호)
+ TPS->>TPS: 시험 대기 상태 진입 (최대 30초)
+ C28-->>TPS: Sentinel C28 측정 완료 최종 데이터 패킷 송신
+ Note over TPS: 프로그램 판정 vs 센서 판정 스펙 교차 검증 수행
+ TPS->>LocalFile: InspectData 로컬 CSV 이중 백업 저장 (UTF-8)
+ TPS->>DBS: 비동기 데이터 저장 요청 (SaveEolDataAsync)
+ DBS->>DB: MERGE INTO [dbo].[EOL] 쿼리 실행 (UPSERT)
+ DBS-->>TPS: 저장 결과 반환 (네트워크 오류 시 로컬 캐시 동작)
+ TPS->>HW_IO: 합격(OK) 또는 불합격(NG) 물리 디지털 출력 제어
+```
+
+---
+
+## 4. 데이터 수집 항목 (Core Data Fields)
+
+시스템에서 계측하고 외부로 영속화하는 데이터의 기본 규격은 다음과 같습니다.
+* **IC_SN / PCB_Barcode**: 판독된 16자리 제품 고유 식별자
+* **Maker / Model / Variant_1 / Variant_2**: 현재 가동 중인 제품 및 생산 기종 정보
+* **Operator**: 로그인된 작업자 ID
+* **Production_Date**: 검사 일시 (DateTime)
+* **Line / Lot_No / Jig_No / Channel**: 작업 라인, 로트 번호, 지그 위치, 검사 채널(좌/우)
+* **Spec_UL / Spec_LL**: 검사 스펙 상한/하한값
+* **Leak_Value**: 실제 계측된 누출값 (Decimal)
+* **Result**: 합격(OK) 또는 불합격(NG) 판정 결과
diff --git a/ob/02_DB_Design_ERD.md b/ob/02_DB_Design_ERD.md
new file mode 100644
index 0000000..9f325b0
--- /dev/null
+++ b/ob/02_DB_Design_ERD.md
@@ -0,0 +1,99 @@
+# [2단계] 데이터베이스 설계 (DB Design & ERD)
+
+본 문서는 누수 시험 시스템에서 취급하는 계측 데이터 및 로그인 사용자 계정 정보를 관리하기 위한 MSSQL 데이터베이스 설계를 정의합니다.
+
+---
+
+## 1. 개체 및 관계 정의 (Entity Relationship Summary)
+
+본 시스템은 단독 장비가 아닌 다수 지그(Jig)와 채널(Left/Right)에서 생산되는 검사 데이터를 수집하므로, 다음의 핵심 엔티티를 관리합니다.
+
+* **Users (사용자 계정)**: 장비를 조작하는 작업자(Operator) 또는 관리자 계정 정보.
+* **EOL (검사 결과 이력)**: 제품의 누수 시험 최종 결과 데이터로, ZMDI/ADLINK 센서로부터 읽어들인 `IC_SN`을 주 식별자로 사용합니다.
+* **SystemConfigs (장비 설정 캐시)**: 프로그램의 상/하한 스펙(SpecUL/LL) 및 통신 포트 옵션 이력.
+
+---
+
+## 2. 테이블 명세서 (Table Specification)
+
+### 2.1 Users (사용자 정보 테이블)
+* **설명**: 장비 로그인 및 조작을 인가받은 사용자 계정을 저장합니다.
+* **테이블명**: `[dbo].[Users]`
+
+| 컬럼명 | 데이터 타입 | Null 여부 | 제약조건 | 설명 |
+| :--- | :--- | :--- | :--- | :--- |
+| **OperatorId** | `VARCHAR(50)` | NOT NULL | PK | 작업자 고유 식별 아이디 (로그인 ID) |
+| **Password** | `VARCHAR(256)` | NOT NULL | - | 단방향 해시화된 비밀번호 (또는 레거시 대조용 암호) |
+| **OperatorName**| `NVARCHAR(50)` | NULL | - | 작업자 실명 |
+| **Role** | `VARCHAR(20)` | NOT NULL | DEFAULT 'OPERATOR' | 사용자 권한 (`ADMIN`, `OPERATOR`, `DEVELOPER`) |
+| **CreatedAt** | `DATETIME` | NOT NULL | DEFAULT GETDATE()| 계정 등록 일시 |
+
+### 2.2 EOL (검사 데이터 저장 테이블)
+* **설명**: 최종 EOL(End of Line) 누수 검사 결과를 기록합니다. 제품 바코드(`IC_SN`)를 기준으로 UPSERT(MERGE) 처리됩니다.
+* **테이블명**: `[dbo].[EOL]`
+
+| 컬럼명 | 데이터 타입 | Null 여부 | 제약조건 | 설명 |
+| :--- | :--- | :--- | :--- | :--- |
+| **IC_SN** | `VARCHAR(50)` | NOT NULL | PK | 센서에서 판독한 16자리 제품 고유 시리얼 번호 |
+| **PCB_Barcode** | `VARCHAR(50)` | NOT NULL | - | 인쇄 회로 기판(PCB) 바코드 (식별 불가 시 `-` 처리) |
+| **Maker** | `NVARCHAR(50)` | NULL | - | 제조사 식별 정보 |
+| **Model** | `NVARCHAR(50)` | NULL | - | 검사 대상 모델 코드 |
+| **Variant_1** | `NVARCHAR(50)` | NULL | - | 파생 기종 식별 속성 1 |
+| **Variant_2** | `NVARCHAR(50)` | NULL | - | 파생 기종 식별 속성 2 |
+| **Operator** | `VARCHAR(50)` | NOT NULL | FK (Users.OperatorId) | 검사를 진행한 로그인 작업자 ID |
+| **Production_Date**| `DATETIME` | NOT NULL | DEFAULT GETDATE() | 검사 수행 완료 일시 |
+| **Line** | `NVARCHAR(30)` | NULL | - | 생산 라인 번호 / 명칭 |
+| **Lot_No** | `NVARCHAR(50)` | NULL | - | 작업 지시 묶음 번호 (Lot Number) |
+| **Jig_No** | `NVARCHAR(20)` | NULL | - | 검사에 사용된 지그(Jig) 번호 |
+| **Channel** | `VARCHAR(10)` | NOT NULL | - | 검사 물리 채널 명칭 (`LEFT`, `RIGHT`) |
+| **Spec_UL** | `DECIMAL(10, 4)`| NOT NULL | - | 합격 스펙 상한치 (Upper Limit) |
+| **Spec_LL** | `DECIMAL(10, 4)`| NOT NULL | - | 합격 스펙 하한치 (Lower Limit) |
+| **Leak_Value** | `DECIMAL(10, 4)`| NOT NULL | - | 센서에서 최종 파싱된 실측 누설값 |
+| **Result** | `VARCHAR(5)` | NOT NULL | CHECK (in ('OK', 'NG'))| 합격(`OK`) 또는 불합격(`NG`) 판정 |
+
+---
+
+## 3. 엔티티 관계도 (Entity Relationship Diagram - ERD)
+
+`EOL` 검사 이력은 `Users` 테이블의 작업자 고유 ID(`OperatorId`)를 외래키(`Operator`)로 참조하여, 어떤 작업자가 어떤 검사 이력을 남겼는지에 대한 일대다(1:N) 식별 및 추적이 가능합니다.
+
+```mermaid
+erDiagram
+ USERS {
+ VARCHAR_50 OperatorId PK "작업자 ID"
+ VARCHAR_256 Password "비밀번호"
+ NVARCHAR_50 OperatorName "작업자 실명"
+ VARCHAR_20 Role "권한 (ADMIN/OPERATOR)"
+ DATETIME CreatedAt "계정 생성일시"
+ }
+
+ EOL {
+ VARCHAR_50 IC_SN PK "제품 시리얼 번호"
+ VARCHAR_50 PCB_Barcode "PCB 바코드"
+ NVARCHAR_50 Maker "제조사"
+ NVARCHAR_50 Model "모델명"
+ NVARCHAR_50 Variant_1 "속성 1"
+ NVARCHAR_50 Variant_2 "속성 2"
+ VARCHAR_50 Operator FK "작업자 ID"
+ DATETIME Production_Date "검사 완료일시"
+ NVARCHAR_30 Line "생산 라인"
+ NVARCHAR_50 Lot_No "로트 번호"
+ NVARCHAR_20 Jig_No "지그 번호"
+ VARCHAR_10 Channel "채널 (LEFT/RIGHT)"
+ DECIMAL Spec_UL "스펙 상한치"
+ DECIMAL Spec_LL "스펙 하한치"
+ DECIMAL Leak_Value "실측 누출량"
+ VARCHAR_5 Result "판정 (OK/NG)"
+ }
+
+ USERS ||--o{ EOL : "performs"
+```
+
+---
+
+## 4. 데이터 정합성 유지 전략 (Data Integrity & Optimization)
+
+* **UPSERT 쿼리 이중화**: 제품 시리얼 `IC_SN`은 고유하므로, 동일 제품이 재검사(Retest)되거나 지연 전송되었을 경우 중복 릴레이션이 맺어지지 않도록 `MERGE` 구문을 활용해 기존 검사 행을 업데이트(UPDATE)하고 신규일 때만 신규 행을 삽입(INSERT)합니다.
+* **인덱스(Index) 설계 전략**:
+ * **Clustered Index**: 데이터가 생성순서에 따라 밀집되도록 `Production_Date`를 포함하거나 주 키인 `IC_SN`에 적용합니다.
+ * **Non-Clustered Index**: 작업 이력 대량 조회(`DataView`)의 검색 속도 최적화를 위해 복합 인덱스 `IX_EOL_Search (Production_Date, Result, Lot_No)` 구성을 제안합니다.
diff --git a/ob/03_API_Specification.md b/ob/03_API_Specification.md
new file mode 100644
index 0000000..b0c0003
--- /dev/null
+++ b/ob/03_API_Specification.md
@@ -0,0 +1,169 @@
+# [3단계] 핵심 API 명세서 설계 (API Specification)
+
+본 문서는 누수 시험 시스템(WPF Client)과 중앙 통합 관리 서버(MES/모니터링 Web API) 간의 통신 규격을 정의하는 RESTful API 명세서입니다.
+
+---
+
+## 1. 인증 API (Authentication)
+
+### 1.1 사용자 로그인 (Sign In)
+* **Method & URL**: `POST /api/v1/auth/login`
+* **설명**: 작업자 ID와 비밀번호를 검증하고 세션 토큰(JWT)을 발급받습니다.
+
+#### 요청 예시 (Request Body)
+```json
+{
+ "operatorId": "test",
+ "password": "testPassword123"
+}
+```
+
+#### 성공 응답 예시 (Response 200 OK)
+```json
+{
+ "success": true,
+ "message": "로그인에 성공했습니다.",
+ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
+ "userInfo": {
+ "operatorId": "test",
+ "operatorName": "홍길동",
+ "role": "OPERATOR"
+ }
+}
+```
+
+#### 실패 응답 예시 (Response 401 Unauthorized - 계정 정보 불일치)
+```json
+{
+ "success": false,
+ "errorCode": "AUTH_FAILED",
+ "message": "등록되지 않은 ID이거나 비밀번호가 일치하지 않습니다."
+}
+```
+
+---
+
+## 2. 계측 데이터 동기화 API (Inspection Data Sync)
+
+### 2.1 검사 데이터 UPSERT 전송 (Upload Inspection Data)
+* **Method & URL**: `POST /api/v1/eol/inspect`
+* **Headers**: `Authorization: Bearer `
+* **설명**: 장비 검사가 완료된 시점에 실시간 누수 측정 값을 서버로 전송합니다. 시리얼 번호가 이미 존재할 경우 업데이트됩니다.
+
+#### 요청 예시 (Request Body)
+```json
+{
+ "icSn": "202606240001LM01",
+ "pcbBarcode": "202606240001LM01",
+ "maker": "MOBI",
+ "model": "NE1aW_PT",
+ "variant1": "V1_STD",
+ "variant2": "V2_REV1",
+ "operator": "test",
+ "line": "LINE_01",
+ "lotNo": "LOT_20260624A",
+ "jigNo": "JIG_A",
+ "channel": "LEFT",
+ "specUL": 1.0000,
+ "specLL": -1.0000,
+ "leakValue": 0.4357,
+ "result": "OK"
+}
+```
+
+#### 성공 응답 예시 (Response 201 Created / 200 OK)
+```json
+{
+ "success": true,
+ "message": "검사 데이터가 정상적으로 저장되었습니다.",
+ "data": {
+ "icSn": "202606240001LM01",
+ "recordedAt": "2026-06-24T10:15:30.123Z"
+ }
+}
+```
+
+#### 실패 응답 예시 (Response 400 Bad Request - 유효하지 않은 데이터 형식)
+```json
+{
+ "success": false,
+ "errorCode": "INVALID_PARAMETERS",
+ "message": "측정 누설값(leakValue)은 필수 수치 정보입니다."
+}
+```
+
+---
+
+## 3. 검사 이력 조회 API (Inspection Data Query)
+
+### 3.1 조건별 검사 기록 목록 조회 (Retrieve History)
+* **Method & URL**: `GET /api/v1/eol/inspects`
+* **Headers**: `Authorization: Bearer `
+* **Query Parameters**:
+ * `startDate`: 조회 시작일 (Format: `YYYY-MM-DD`)
+ * `endDate`: 조회 종료일 (Format: `YYYY-MM-DD`)
+ * `result`: 판정 필터 (`OK`, `NG`, 생략 시 전체)
+ * `lotNo`: 로트 번호 필터 (일부분 검색 가능)
+ * `operator`: 작업자 ID 필터
+
+#### 성공 응답 예시 (Response 200 OK)
+```json
+{
+ "success": true,
+ "totalCount": 2,
+ "results": [
+ {
+ "icSn": "202606240001LM01",
+ "pcbBarcode": "202606240001LM01",
+ "maker": "MOBI",
+ "model": "NE1aW_PT",
+ "variant1": "V1_STD",
+ "variant2": "V2_REV1",
+ "operator": "test",
+ "productionDate": "2026-06-24T10:15:30Z",
+ "line": "LINE_01",
+ "lotNo": "LOT_20260624A",
+ "jigNo": "JIG_A",
+ "channel": "LEFT",
+ "specUL": 1.0000,
+ "specLL": -1.0000,
+ "leakValue": 0.4357,
+ "result": "OK"
+ },
+ {
+ "icSn": "202606240002LM02",
+ "pcbBarcode": "202606240002LM02",
+ "maker": "MOBI",
+ "model": "NE1aW_PT",
+ "variant1": "V1_STD",
+ "variant2": "V2_REV1",
+ "operator": "test",
+ "productionDate": "2026-06-24T10:16:15Z",
+ "line": "LINE_01",
+ "lotNo": "LOT_20260624A",
+ "jigNo": "JIG_A",
+ "channel": "RIGHT",
+ "specUL": 1.0000,
+ "specLL": -1.0000,
+ "leakValue": 1.2541,
+ "result": "NG"
+ }
+ ]
+}
+```
+
+---
+
+## 4. HTTP 상태 코드 정의 가이드 (Status Codes Definition)
+
+본 연동 API 규격은 직관적이고 표준적인 HTTP Response 상태 코드를 따릅니다.
+
+| 상태 코드 | 의미 | 사용 시점 |
+| :--- | :--- | :--- |
+| **200 OK** | 성공 | 조회(GET) 성공 및 정상 데이터 업데이트(UPSERT) 성공 시 |
+| **201 Created** | 생성 성공 | 신규 검사 데이터 삽입(INSERT)이 데이터베이스에 성공 시 |
+| **400 Bad Request** | 잘못된 요청 | 파라미터 누락, 데이터 타입 에러 등 요청 양식이 규칙에 맞지 않을 때 |
+| **401 Unauthorized**| 권한 없음 | 로그인 토큰(Bearer Token)이 만료되었거나 누락되어 인증이 안 된 경우 |
+| **403 Forbidden** | 접근 거부 | 일반 작업자 권한으로 관리자 API에 직접 접근을 시도할 때 |
+| **404 Not Found** | 리소스 없음 | 요청한 엔드포인트 URL 또는 조회 대상 제품 ID를 찾을 수 없을 때 |
+| **500 Internal Server Error** | 서버 오류 | 서버 DB 연결 이상, 데이터베이스 데드락, 런타임 처리 오류 발생 시 |
diff --git a/ob/04_Security_Scalability.md b/ob/04_Security_Scalability.md
new file mode 100644
index 0000000..2b0736b
--- /dev/null
+++ b/ob/04_Security_Scalability.md
@@ -0,0 +1,64 @@
+# [4단계] 보안, 확장성 및 예외 전략 (Security & Scalability)
+
+본 문서는 누수 시험 시스템의 비기능적 요구사항(보안, 가용성, 성능 확장성 및 예외 장애 대응 정책)을 정의한 아키텍처 가이드라인입니다.
+
+---
+
+## 1. 보안 및 인가 전략 (Security & Encryption)
+
+### 1.1 계정 보안 및 비밀번호 해시화
+* **전송 보안**: 클라이언트(WPF)와 중앙 API 서버 간의 모든 데이터 전송은 **HTTPS(TLS 1.2 이상)** 프로토콜을 필수로 사용하여 스니핑 공격을 차단합니다.
+* **비밀번호 저장**: 데이터베이스 내 `Users.Password` 필드는 솔트(Salt)를 포함한 **PBKDF2-SHA256** 또는 **BCrypt** 알고리즘으로 단방향 해시 처리하여 데이터베이스 노출 시에도 비밀번호 복호화를 불가능하게 합니다.
+* **인증 관리**: API 호출 시 Stateless한 **JWT (JSON Web Token)** 방식을 적용하고, 토큰 만료 시간은 8~12시간으로 제한하여 탈취 위험을 최소화합니다.
+
+### 1.2 장비 통신 물리 보안 및 인가
+* 공장 폐쇄망 내부 IP 화이트리스트(IP Whitelisting) 정책을 적용하여, 사전 인가된 장비 제어 PC 및 관리 포트 외의 서버 접근을 원천 차단합니다.
+
+---
+
+## 2. 가용성 및 트래픽 성능 확장 전략 (Scalability)
+
+### 2.1 로컬 버퍼링 & 재시도 (Network Failover)
+공장 내 네트워크 불안정으로 인해 중앙 데이터베이스 또는 API 서버 접속이 일시 차단될 경우, 실시간 EOL 공정이 중단되지 않도록 하는 전략입니다.
+
+```mermaid
+graph TD
+ Test[검사 완료] --> Check{네트워크 정상?}
+ Check -->|Yes| DB[MSSQL 또는 API 업로드 성공]
+ Check -->|No| Local[로컬 이중화 파일 저장]
+ Local --> Queue[비동기 재전송 큐 삽입]
+ Queue --> Scheduler[백그라운드 복구 스케줄러]
+ Scheduler --> Retry{연결 복구됨?}
+ Retry -->|Yes| Sync[벌크 업로드 및 로컬 캐시 정리]
+ Retry -->|No| Wait[대기 후 재시도]
+```
+
+* **Offline Fallback**: 네트워크 장애 감지 시 `EolDatabaseService` 및 `LogParser`는 로컬 CSV 파일 쓰기 모드로 즉시 Fallback을 수행합니다.
+* **Background Sync**: 네트워크 복구 감지 시, 로컬 CSV에만 남겨진 계측 미송신 데이터를 백그라운드 스레드에서 순차적으로 읽어와 REST API 또는 Bulk Copy를 통해 최종 SQL 데이터베이스에 누락 없이 업로드합니다.
+
+### 2.2 쿼리 성능 최적화 및 인덱스 관리
+* **장비 데이터 파티셔닝**: `EOL` 테이블의 크기가 수천만 건 이상 비대해질 경우를 대비해 `Production_Date` 연/월 단위로 파티셔닝(Partitioning)하여 쿼리 검색 범위를 국소화합니다.
+* **배치(Batch) 처리**: 이력 조회 대용량 엑셀 다운로드나 검색 시 DB 락(Lock) 점유 시간을 낮추기 위해 `WITH (NOLOCK)` 힌트 적용 및 대량 삽입 시 배치 쿼리를 권장합니다.
+
+---
+
+## 3. 예외 장애 처리 및 통합 로깅 정책 (Exception & Logging)
+
+### 3.1 하드웨어 예외 처리 가이드라인
+* **시리얼 통신 끊김**: Sentinel C28 또는 ADLINK 보드 통신 시 타임아웃 발생 시, 스레드가 멈추지 않도록 비동기 타임아웃 토큰(`CancellationToken`)을 활용해 강제 반환 후 로그 파일에 기록하고 UI 화면 상태바에 적색 경고를 표시합니다.
+* **포트 오동작 복구**: 예외 복구를 위해 포트 리커버리 타이머(`ReconnectTimer`)를 동작시켜, 물리 케이블 재연결 시 윈도우가 자동으로 재연결을 수립(Auto-Reconnection)할 수 있도록 설계합니다.
+
+### 3.2 통합 로깅 정책 (Standard Logging Policy)
+모든 모듈의 예외와 동작 이력은 파일 로거(`FileLogger`) 및 중앙 서버 로거에 표준 규격으로 정주되어 기록되어야 합니다.
+
+#### 로깅 레벨 정의 (Logging Levels)
+1. **INFO**: 시스템 정상 기동, 포트 오픈 성공, 로그인 성공 등 일반적인 이벤트 추적용.
+2. **WARNING**: 센서 ID 읽기 일시 실패 후 재시도 성공, 타임아웃 1회 발생, 스펙 교차 검증 불일치 등 경고성 사항.
+3. **ERROR**: 데이터베이스 연결 실패(Fallback 동작 실행됨), 시리얼 포트 바인딩 실패, 하드웨어 오작동 등 기능적 오류.
+4. **FATAL**: 복구 불가능한 시스템 크래시, 프로그램 비정상 종료 등 즉각적인 조치가 필요한 상황.
+
+#### 표준 로그 포맷 규격
+```text
+[yyyy-MM-dd HH:mm:ss.fff] [LOG_LEVEL] [Class_Name.Method_Name] Message (Details...)
+예: [2026-06-24 10:15:45.302] [ERROR] [Board4251Service.SendCommandAsync] Timeout waiting for response (Retry 3/3). Command: x00c_001101:ow2800326003e
+```
diff --git a/ob/05_Implementation_History.md b/ob/05_Implementation_History.md
new file mode 100644
index 0000000..1223d52
--- /dev/null
+++ b/ob/05_Implementation_History.md
@@ -0,0 +1,106 @@
+# [5단계] 프로젝트 전체 구현 현황 및 개발 과정 이력 (Project Implementation & Development History)
+
+본 문서는 Windows 기반 Air Leak Test 장비의 제어 및 모니터링 시스템(WPF Leak Test System)의 전체적인 기능 명세, 하드웨어 연동 규격, 그리고 프로젝트가 시작된 이래의 개발 이력 전체를 포괄하여 기록한 문서입니다.
+
+---
+
+## 1. 프로젝트 개요 (Project Summary)
+본 시스템은 생산 라인(EOL)의 Air Leak Test(기밀 시험) 장비를 직접 제어하고, 실시간 계측값을 수집하여 합불 판정(OK/NG)을 내리는 Windows 데스크톱 GUI 프로그램입니다.
+* **구조**: WPF(C#) 기반의 MVVM 패턴을 충족하며, UI 컴포넌트(View)와 비즈니스 서비스(Service), 하드웨어 통신 드라이버가 서로 역할 분리(Decoupling)되어 있습니다.
+* **통신 핵심**: RS-232/Serial 기반의 누수 감지 센서 및 ADLINK DIO/ID 판독용 스마트 보드와의 실시간 동기 및 제어를 지원합니다.
+
+---
+
+## 2. 프로젝트 전체 핵심 기능 명세 (Core Features)
+
+### 2.1 실시간 시험 모니터링 (Home View)
+* **독립 2채널 제어**: 좌(LEFT) 및 우(RIGHT) 채널이 개별 워커 스레드로 구동되어 상호 간섭 없이 동시에 또는 순차적으로 기밀 시험을 제어하고 측정값(sccm)을 시각화합니다.
+* **실시간 상태 지시**: 판정 결과(`OK`=녹색, `NG`=적색)에 따라 각 채널 카드배경이 반응형으로 자동 전환되며, 하드웨어 레벨의 진행 상태 및 에러 메시지를 표시합니다.
+* **센서 ID 실시간 매핑**: 각 채널 아래 제품 ID(제조년월일, 시리얼, 제품 모델 기종 등)를 실시간 바인딩합니다.
+* **독립 ID 테스트**: 좌/우 별도의 'ID Test' 동작 모드를 지원하여 시험 프로세스 동작 없이 센서와의 단독 연결 및 데이터 복구 상태를 점검할 수 있습니다.
+
+### 2.2 자동 시험 제어 프로세스 (TestProcessService Loop)
+생산 장비의 DIO 시작 스위치 물리 접점 신호를 수집하여 작동하는 백그라운드 스레드 제어 루프입니다.
+1. **시작 신호 트리거**: `LEFT_START` / `RIGHT_START` 접점 신호 OFF $\rightarrow$ ON 감지 시 락(Lock) 획득 및 기상.
+2. **제품 정보 판독**: `Board4251SensorService`를 통해 제품 센서 메모리에서 16자리 ID 취득.
+3. **LEAK 시험 및 수집**: Sentinel C28 누수 측정기로부터 최종 패킷 수신 대기 (최대 30초).
+4. **교차 검증 및 판정**: 설정된 상하한 스펙(SpecUL/LL) 기반 합불 판정과 C28 기기 자체 판정(Accept/Reject)을 비교 검증(`SpecMismatch` 감지 시 경고 팝업).
+5. **데이터 영속화**: 로컬 CSV 로그와 MSSQL 서버 데이터베이스에 이중 쓰기 수행.
+6. **디지털 물리 출력**: 판정 결과에 맞춰 `LEFT_OK` / `LEFT_NG` 등 하드웨어 포트에 전송.
+
+### 2.3 입출력 모니터링 (In/Out Monitor)
+* **실시간 접점 스캔**: ADLINK DIO 입력 접점(32비트) 및 출력 접점의 상태를 500ms 주기로 스캔하여 비주얼 온/오프 상태를 화면에 표출합니다.
+* **페이징 제공**: 30개씩 분할 표출하는 페이징 제어와 화면 종료 시 불필요한 폴링 타이머 자원 해제 루틴을 지원합니다.
+
+### 2.4 데이터 조회 및 CSV 내보내기 (Data History)
+* **고성능 조회 및 필터링**: 기간, 판정 결과, 작업자, 로트 번호, 모델 등의 조건으로 MSSQL 이력 데이터를 신속 조회합니다. (DB 단절 시 로컬 일일 CSV 백업 파일 자동 파싱 Fallback 내장)
+* **격자 스타일 유지**: 데이터가 전혀 없는 상태에서도 엑셀 양식과 동일하게 최소 50행의 빈 격자를 유지하여 UI 일관성을 확보합니다.
+
+### 2.5 환경 설정 및 핫 리로드 (Configuration & Hot Reload)
+* **XML 설정 파일 (`config.xml`)**: C28 센서 통신 포트 및 보드레이트, ADLINK 보드 번호 및 연결 대기 시간 등을 영속 저장합니다. 설정 갱신 시 `ConfigChanged` 이벤트를 통해 가동 중인 스레드를 안전하게 자동 재시작(Hot Reload)합니다.
+* **INI 설정 파일 (`Settings/DioConfig.ini`)**: DIO 보드의 모델과 접점 이름, 핀 설명 매핑 테이블을 관리하며 프로그램 재작동 시 해당 구조로 동적 빌딩됩니다.
+
+---
+
+## 3. 기기 통신 프로토콜 및 예외 처리 규격 (Hardware Interface)
+
+### 3.1 Sentinel C28 (Leak Test 장비)
+* **통신 조건**: RS-232, 9600 Baud, 8N1.
+* **프레임 구조**: `[CRC-8 체크섬 (2자리 Hex)][Sequence 번호 (2자리 Hex)][Payload 길이 (3자리 Hex)] [Data Type][Data Payload]\r\n`
+* **교차 판정**: C28 기기의 자체 판정값(Pass/Fail/Accept/Reject)과 프로그램 상하한 스펙 판정 결과를 A/R 형식으로 정규화하여 교차 검증을 강제합니다.
+
+### 3.2 ADLINK Board 4251 (ID 판독기 및 컨트롤러)
+* **채널 다중화**: 1개의 시리얼 포트를 좌/우 채널이 공유하며, 패킷 헤더 내 채널 번호(`001`=좌, `002`=우)로 식별합니다.
+* **ID 판독 2단계**:
+ 1. 보드 상태 조회 명령: `x00c_00{ch}101:owt28006727ea97c7801\r\n` $\rightarrow$ `Success` 시 다음 단계 진행.
+ 2. 16자리 ID 조회 명령: `x00c_00{ch}101:ow2800326003e\r\n` $\rightarrow$ `[A-Za-z0-9]{16}` 매칭 필터링.
+* **끝자리 'F' 재시도 알고리즘**:
+ * 제품 ID를 읽었을 때 끝자리가 'F'인 경우, 데이터 불안정 오류일 수 있으므로 **350ms 간격으로 최대 3회(초회 포함)**까지 재시도 통신을 수행합니다. 3회 모두 'F'일 경우 최종 실패 경고 로깅 후 그대로 전달합니다.
+
+---
+
+## 4. 전체 개발 이력 연혁 (Development Timeline History)
+
+### 📅 2026-04-10 (4251 보드 통합 및 단독 테스트 기능 탑재)
+* **내용**: 좌우 기밀 센서의 시리얼 통신 포트를 하나의 COM 포트로 묶는 듀얼 채널 통합(`Board4251Service.cs`)을 구현했습니다.
+* **기능**: ID 센서 자체 상태를 확인하여 `Fail` 시 동작을 중단하는 게이트키퍼 코드를 안착시키고, 현장에서 센서만 별도로 연결 상태를 점검할 수 있는 `ExecuteSensorTestAsync` (ID Test L/R) 수동 모드를 추가하였습니다.
+
+### 📅 2026-04-13 (측정 타임아웃 고도화 및 통신 리트라이 고안)
+* **내용**: 기계식 누수 계측 도중 30초 시간 초과(Timeout)가 나면 로그 및 데이터 유실이 발생하던 문제를 해결했습니다.
+* **기능**: 타임아웃 발생 시에도 `SensorJudgment="T/O"`, `Judgment="NG"`로 명확히 판정을 세팅하여 실시간 화면 Grid 및 일일 CSV 로그 파일에 기록하도록 개선하고, 이 시점에도 DIO 하드웨어 단에 불합격(NG) 디지털 출력을 물리 전송하게 하였습니다.
+* **기능**: 4251 보드에서 간헐적으로 오동작 값이 수집되던 이슈를 방지하기 위해 **끝자리 'F' 판별 재시도 알고리즘(최대 3회)**을 개발해 연동했습니다.
+
+### 📅 2026-06-22 ~ 2026-06-23 (사용자 로그인 및 DB 프로시저 확장)
+* **내용**: DB 사용자 로그인 기능 및 사내 보안 프로시저 연동 스펙을 접목했습니다.
+* **기능**: `Database.ini` 파일 설정값에 따라 사내 MSSQL 데이터베이스에 등록된 `Users` 테이블 조회를 일반 쿼리와 저장 프로시저 호출 방식으로 동적 지원하게 구현했습니다.
+
+### 📅 2026-06-24 (성능 고도화, SRP 파일 분해, DI 연동 및 다이얼로그 추상화)
+* **내용**: 공장 24시간 가동 환경에서의 리소스 누수 및 불필요 오버헤드를 타파하기 위한 솔루션 리팩토링 단계입니다.
+* **기능**:
+ 1. 검사 저장 시마다 발생하던 `Database.ini` 파싱 디스크 I/O 최적화 (시간 감지 캐시 적용).
+ 2. 스레드 폴링 부하(`Thread.Sleep(2)`)를 `AutoResetEvent` 대기 구조로 마이그레이션 (대기 시 CPU 점유율 0%).
+ 3. `LoginServicesStub.cs` 파일에 들어있던 4개 클래스를 개별 독립 파일로 분리하고 구형 csproj에 정상 바인딩.
+ 4. 수동 문자열 결합 JSON 이력 관리를 단일 닷넷 어셈블리 `XmlSerializer` 방식으로 교체하여 의존성 없는 안전성 확보.
+ 5. `EolDatabaseService`에서 `App.LoginResult` 전역 참조를 차단하고 파라미터 주입형태로 리팩토링.
+ 6. 뷰모델 및 헬퍼 파일 내 WPF `MessageBox.Show` 호출부를 `IDialogService` 인터페이스로 격리 설계 (비-UI 환경 단위 테스트 자동화 지원).
+ 7. 리팩토링 최종 결과 MSBuild를 통해 0 warnings, 0 errors 빌드 무결성 검증 통과.
+ 8. 최신 개편 스펙(16개 컬럼 및 MERGE)에 맞추어 DB 스키마 정의 파일(`dbo.EOL_Schema.sql`) 동기화 완료.
+ 9. **[금일 추가]** 데이터베이스(MSSQL) 저장 실패 시 기존 검사 화면의 "오류 메시지" TextBox 영역에 실시간으로 `"DB 저장 실패"` 에러 문구를 노출하도록 예외 피드백 흐름 구현 및 검증 완료.
+
+### 📅 2026-06-25 (배포 편의성 향상, DB 개별 키 파싱 지원, 설정 파일 가독성 고도화, 데이터 조회 오류 수정 및 CSV-DB 저장 정합성 일원화)
+* **내용**: 배포 대상 컴퓨터에 프로그램 설치 시 `Database.ini` 설정 파일이 누락되어 생기는 오류를 방지하고, 모든 설정 파일의 포맷을 사람이 읽기 쉽도록 개선했습니다. 또한 데이터 이력 조회 화면의 조회 실패 오류 및 버그(파일 잠금, 날짜 포맷 로캘 불일치)를 예방하기 위한 정교한 방어적 프로그래밍을 적용했으며, 로컬 CSV와 데이터베이스 저장 시 발생하던 데이터 필드 불일치(Mismatch) 현상을 완벽하게 동기화했습니다.
+* **기능**:
+ 1. **설정 자동 생성**: 프로그램 시작 시점(`App.xaml.cs`의 `OnStartup`)에 `Database.ini`가 없을 경우 기본 접속 템플릿 정보(MSSQL 설정 및 로그인 프로시저 양식)를 담은 파일을 자동 생성하도록 예외 방지 및 파일 생성 로직을 구축했습니다.
+ 2. **DB 개별 설정 파싱 및 결합**: 기존 한 줄로 길게 늘어지던 `ConnectionString` 대신, 사용자가 직관적으로 여러 줄에 작성할 수 있도록 `Db_Server`, `Db_Name`, `Db_User`, `Db_Password` 개별 키를 파싱하는 구조(`DatabaseSettings.cs`)로 개선했습니다. (이전 방식의 `ConnectionString` 통째 입력 방식도 하위 호환을 위해 Fallback 지원)
+ 3. **가독성 향상 및 주석화**: 자동 생성 및 실제 로컬 파일들의 `Database.ini`와 `Settings/DioConfig.ini` 파일에 넉넉한 줄바꿈(개행) 및 상세한 주석 설명을 달아, 사람이 직접 열어서 편집할 때 직관적으로 파악할 수 있도록 서식을 대폭 수정했습니다.
+ 4. **XML 정밀 포맷팅**: `config.xml` 저장 시(`ConfigManager.Save`), 불필요한 XML 네임스페이스를 소거하고 들여쓰기 4칸(`Indent = true`, `IndentChars = " "`)을 강제하여 XML 구조 가독성을 고도화했습니다.
+ 5. **데이터 조회 안정성 및 예외 처리 강화**:
+ * **공유 파일 읽기(FileShare)**: 현장 작업자가 로컬 백업 CSV 파일을 엑셀 등으로 열어 두어 파일 잠금(Lock)이 걸렸을 때, 이력 조회 기능에서 발생하던 파일 접근 오류(`IOException`)를 해결하기 위해 `FileShare.ReadWrite` 모드로 CSV 파일을 안전하게 읽어오도록 수정했습니다.
+ * **로캘 독립적 날짜 파싱**: Windows의 시스템 언어/문화권 설정(Locale)에 따라 날짜 포맷이 파싱되지 않는 문제를 예방하고자, 파일명 및 CSV 내부 날짜 데이터 해석 시 `DateTime.TryParseExact`를 명시하여 해외 공장 PC 등 어떠한 OS 환경에서도 날짜가 안정적으로 필터링되도록 보장했습니다.
+ * **DB 쿼리 시간 범위 경계값 정밀화**: `DATETIME2(0)`의 소수점 밀리초 오차로 인한 경계 시점(23시 59분 59초 등) 데이터 누락을 원천 차단하기 위해, SQL 조건식을 `Production_Date < @EndLimit` 형식으로 변경하고 익일 00:00:00 미만으로 설정하여 인덱스 스캔의 안정성과 조회 신뢰도를 향상시켰습니다.
+ * **로컬 CSV 조회 시 중복 데이터 최신화 필터링**: 동일한 제품 일련번호(`IcSn`/`ProductId`)로 재검사를 진행하는 경우, DB는 `MERGE` 구문에 의해 최종 결과로 덮어쓰기(UPDATE)되는 반면 CSV는 계속 행이 누적되는 차이로 인해 발생할 수 있는 데이터 조회 불일치를 보완했습니다. CSV 조회 엔진(`ParseLogsFromCsv`)에서 동일 제품에 대해서는 가장 최근 날짜/시간의 1건만 화면에 출력하도록 필터링을 적용하여 DB와 정합성을 일치시켰습니다. (식별 불가 코드인 `"-"` 및 공란 데이터는 필터링에서 제외)
+ 6. **로컬 CSV 및 DB 저장 데이터 일치성 보장**:
+ * **InspectData 메타데이터 주입**: `TestProcessService.cs`에서 합격, 불합격(Fail), 타임아웃(Timeout) 판정 데이터를 로컬 CSV에 쓸 때 로그인 정보(Maker, Model, Variant, Operator 등)를 누락하고 쓰던 문제를 해결하기 위해, 인스턴스 생성 시 로그인 정보와 개별 `IcSn`, `PcbBarcode` 및 `ProductionDate`를 완벽하게 기입하여 CSV에도 빈틈없이 기록되도록 동기화했습니다.
+ * **DB 파라미터 정합성 확보**: `EolDatabaseService.cs`에서 레거시 `ProductId`를 `@IC_SN`과 `@PCB_Barcode`에 중복 할당하던 로직을 변경하여, 매칭된 `IcSn`과 `PcbBarcode`를 독립적으로 바인딩하고, 시간 정보도 CSV에 기록된 것과 동일하게 `inspectData.ProductionDate`를 사용하도록 수정하여 두 스토리지 간의 데이터 정합성을 일치시켰습니다.
+
+---
diff --git a/ob/README.md b/ob/README.md
new file mode 100644
index 0000000..d0230fc
--- /dev/null
+++ b/ob/README.md
@@ -0,0 +1,10 @@
+# Mobi Leak Test Project - Knowledge Base (지식베이스)
+
+이 디렉터리는 프로젝트의 DB 연동, 로그인 검증, 검사 프로세스 등에 대한 설계 사양과 구현 세부 사양을 관리하는 지식베이스입니다.
+
+## 문서 목록
+
+1. [로그인 프로시저 연동 및 검사 결과 저장 개편 사양서](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/ob/db_login_integration.md)
+ - `dbo.CheckOperator` 저장 프로시저 연동 설계 및 구현 내용
+ - `dbo.EOL` 데이터베이스 테이블 신규 구성에 따른 결과 저장 로직 매핑 구조
+ - Non-SDK Style C# WPF 프로젝트 빌드 검증 가이드 및 주의사항
diff --git a/ob/System_Design_Guide.md b/ob/System_Design_Guide.md
new file mode 100644
index 0000000..bdcca8b
--- /dev/null
+++ b/ob/System_Design_Guide.md
@@ -0,0 +1,40 @@
+# 시스템 설계 프로세스 가이드
+
+본 문서는 수석 솔루션 아키텍트이자 테크니컬 라이터의 관점에서 시스템 설계를 단계적으로 문서화하고 추적하기 위한 가이드라인입니다.
+
+---
+
+## 📌 역할 설정
+* **역할**: 복잡한 비즈니스 요구사항을 안정적이고 확장 가능한 기술 구조로 설계하는 10년 차 이상의 '수석 솔루션 아키텍트(Solution Architect)' 및 '테크니컬 라이터(Technical Writer)'.
+* **목표**: 개발자와 인프라 엔지니어가 즉시 보고 실무에 투입될 수 있는 수준의 고품질 마크다운 설계 문서를 점진적으로 완성해 나갑니다.
+
+---
+
+## 🔁 단계별 설계 진행 프로세스
+모든 단계를 한 번에 작성하지 않으며, 각 단계가 완료될 때마다 사용자 승인을 득한 후 다음 단계로 전개합니다.
+
+### 1단계: 아키텍처 및 기술 스택 정의 (System Architecture)
+* 시스템의 전체 물리적/논리적 구조 및 데이터 흐름 정의.
+* 클라이언트, 서버, 데이터베이스, 인프라 스택 제안 및 타당성 분석.
+* Mermaid.js 아키텍처 다이어그램 및 데이터 흐름도 시각화.
+
+### 2단계: 데이터베이스 설계 (DB Design & ERD)
+* 핵심 엔티티 및 테이블 명세와 릴레이션 정의.
+* 각 테이블 필드명, 타입, 제약조건 명문화.
+* Mermaid.js `erDiagram`을 활용한 비주얼 ERD 작성.
+
+### 3단계: 핵심 API 명세서 설계 (API Specification)
+* HTTP 메서드, RESTful API 엔드포인트 도출.
+* JSON 형식의 입출력 Request/Response 포맷 정의.
+* HTTP 상태 코드 정의 (성공/실패 케이스).
+
+### 4단계: 보안, 확장성 및 예외 전략 (Security & Scalability)
+* JWT/OAuth2 등 보안 인증 및 인가 방식 설계.
+* 데이터 암호화, 이중화, 캐싱(Redis), DB 로드밸런싱 가이드라인.
+* 예외 처리 정책 및 통합 로깅 정책 수립.
+
+---
+
+## 📝 옵시디언(Obsidian) 문서화 가이드
+* 모든 산출물은 마크다운 형식을 따르며, 코드 블록과 테이블, 다이어그램을 적극 사용합니다.
+* 개발 전/후 읽기 및 쓰기 동작을 이 가이드 문서 및 파생 문서들을 통해 수행합니다.
diff --git a/ob/db_login_integration.md b/ob/db_login_integration.md
new file mode 100644
index 0000000..545108e
--- /dev/null
+++ b/ob/db_login_integration.md
@@ -0,0 +1,151 @@
+# 로그인 프로시저 연동 및 검사 결과 저장 개편 사양서
+
+본 문서는 로그인 검증 시 특정 저장 프로시저를 연동하고, 검사 완료 결과를 신규 정의된 `dbo.EOL` 테이블 스키마 및 16컬럼 로컬 CSV 구조에 맞추어 통합 저장하도록 변경한 설계 및 구현 상세 사양을 정리한 기술 지식베이스입니다.
+
+---
+
+## 1. 로그인 프로시저 연동 설계 및 구현
+
+### 1.1 설정 사양 (`Database.ini`)
+프로그램 구동 시 로그인 및 DB 접속 세부 사양을 유연하게 제어하기 위해 프로그램 실행 디렉터리(`AppContext.BaseDirectory`) 아래의 `Database.ini` 파일을 파싱하여 설정을 로드합니다.
+
+* **파일 위치**: `Database.ini`
+* **설정 형식**:
+ 사람이 직접 읽고 편집하기 편리하도록 개별 상세 접속 파라미터(`Db_Server`, `Db_Name`, `Db_User`, `Db_Password`)로 줄바꿈하여 설정할 수 있습니다. (하위 호환성을 위해 단일 `ConnectionString` 통째 입력 방식도 동시에 지원합니다.)
+ ```ini
+ ; [데이터베이스 접속 상세 설정]
+ Db_Server=192.168.0.20
+ Db_Name=NE1aW_PT_SENSOR
+ Db_User=sa
+ Db_Password=sa1234
+
+ [Login]
+ Procedure=dbo.CheckOperator
+ ProcessName=EOL
+ OfflinePreview=false
+ ```
+
+### 1.2 소스코드 변경 내역
+
+#### [LoginSettings.cs](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Services/LoginServicesStub.cs#L45-L113)
+* **추가 프로퍼티**:
+ * `Procedure` (string): 검증에 사용할 DB 저장 프로시저명
+ * `ProcessName` (string): 검사에 연동된 공정명
+* **로드 로직**: `Database.ini` 파일로부터 `Procedure` 및 `ProcessName` 키를 대소문자 구분 없이 인식하여 설정 개체에 바인딩합니다.
+
+#### [LoginAccountRepository.cs (ExistsAsync)](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Services/LoginServicesStub.cs#L240-L312)
+* **동작 흐름**:
+ 1. 데모 계정(`test`/`test`)을 입력한 경우 DB 조회나 설정 여부와 상관없이 최우선적으로 로그인을 즉시 통과시킵니다. (기존의 `admin`, `su` 데모 계정은 제거되었습니다.)
+ 2. `OfflinePreview`가 `true`이면 DB 조회를 건너뛰고 무조건 로그인을 성공 처리합니다.
+ 3. `Database.ini`에 `Procedure` 설정이 정의되어 있으면 `SqlCommand` 객체의 `CommandType`을 `StoredProcedure`로 지정하여 해당 프로시저를 호출합니다.
+ * **SQL 매개변수**:
+ * `@OperatorId`: 사용자 입력 ID
+ * `@Password`: 사용자 입력 패스워드
+ * `@ProcessName`: INI 파일의 `ProcessName` 정보 (`EOL`)
+ * **인증 판단**: 프로시저 반환값을 정수로 파싱하여 결과가 `0`보다 크면 성공(`true`), 그렇지 않으면 실패(`false`)로 판단합니다.
+ 4. `Procedure` 설정이 없을 경우 Fallback 쿼리(`SELECT COUNT(*) FROM [Users] WHERE [OperatorId] = @Id ...`)를 작동시킵니다.
+ 5. DB 연결 또는 쿼리 오류 등으로 인해 SQL Exception이 발생하더라도 프로그램 비정상 종료를 예방하는 예외 격리 구조를 갖추고 있습니다.
+
+---
+
+## 2. 검사 결과 저장 개편 (`dbo.EOL` 스키마 및 CSV 일원화 대응)
+
+### 2.1 데이터베이스 구성 명세 (`dbo.EOL`)
+사용자가 정의한 구성안에 맞춰 `Password` 컬럼을 완전히 삭제하고, 검사 결과 및 전역 로그인 메타데이터 정보를 릭(Leak) 검사 결과와 병합하여 UPSERT(MERGE) 처리합니다.
+
+| 물리 컬럼명 | 데이터 타입 | 매핑 설명 | 비고 |
+| :--- | :--- | :--- | :--- |
+| **`IC_SN`** | `NVARCHAR(10)` | 제품의 최종 조합 ID (`inspectData.IcSn`) | **PK / FK** |
+| **`PCB_Barcode`** | `NVARCHAR(10)` | 고유 식별용 바코드 정보 | **UK** *(inspectData.PcbBarcode 매핑)* |
+| `Maker` | `NVARCHAR(10)` | 로그인 메이커 정보 (`App.LoginResult.Maker`) | |
+| `Model` | `NVARCHAR(10)` | 로그인 모델 정보 (`App.LoginResult.Model`) | |
+| `Variant_1` | `NVARCHAR(10)` | 로그인 변형1 정보 (`App.LoginResult.Variant1`) | |
+| `Variant_2` | `NVARCHAR(10)` | 로그인 변형2 정보 (`App.LoginResult.Variant2`) | |
+| `Operator` | `NVARCHAR(10)` | 로그인 검사자 ID (`App.LoginResult.Operator`) | |
+| `Production_Date` | `DATETIME2(0)` | 검사 일시 (`inspectData.ProductionDate ?? DateTime.Now`) | |
+| `Line` | `NVARCHAR(5)` | 로그인 작업 라인 (`App.LoginResult.LineNo`) | |
+| `Lot_No` | `NVARCHAR(8)` | 로그인 Lot 번호 (`App.LoginResult.LotNo`) | |
+| `Jig_No` | `NVARCHAR(10)` | 로그인 지그 번호 (`App.LoginResult.JigNo`) | |
+| `Channel` | `NVARCHAR(5)` | 검증 수행 채널 (`LEFT` / `RIGHT`) | |
+| `Spec_UL` | `DECIMAL(3,2)` | 스펙 상한값 (`decimal` 변환) | |
+| `Spec_LL` | `DECIMAL(3,2)` | 스펙 하한값 (`decimal` 변환) | |
+| `Leak_Value` | `DECIMAL(5,4)` | 측정된 누설량 (`decimal` 변환) | |
+| `Result` | `NVARCHAR(5)` | 합격 여부 판정 (`OK` / `NG`) | |
+
+### 2.2 SQL UPSERT 쿼리 명세 (MERGE 구문)
+`IC_SN` 키를 기준으로 이미 데이터가 적재되어 있다면 모든 관련 필드를 신규 검사 정보로 `UPDATE` 하고, 미등록 상태라면 새로 `INSERT` 하도록 설계되었습니다.
+
+```sql
+MERGE INTO [dbo].[EOL] AS Target
+USING (SELECT @IC_SN AS IC_SN) AS Source
+ON (Target.IC_SN = Source.IC_SN)
+WHEN MATCHED THEN
+ UPDATE SET
+ Target.PCB_Barcode = @PCB_Barcode,
+ Target.Maker = @Maker,
+ Target.Model = @Model,
+ Target.Variant_1 = @Variant_1,
+ Target.Variant_2 = @Variant_2,
+ Target.Operator = @Operator,
+ Target.Production_Date = @Production_Date,
+ Target.Line = @Line,
+ Target.Lot_No = @Lot_No,
+ Target.Jig_No = @Jig_No,
+ Target.Channel = @Channel,
+ Target.Spec_UL = @Spec_UL,
+ Target.Spec_LL = @Spec_LL,
+ Target.Leak_Value = @Leak_Value,
+ Target.Result = @Result
+WHEN NOT MATCHED THEN
+ INSERT (IC_SN, PCB_Barcode, Maker, Model, Variant_1, Variant_2, Operator, Production_Date, Line, Lot_No, Jig_No, Channel, Spec_UL, Spec_LL, Leak_Value, Result)
+ VALUES (@IC_SN, @PCB_Barcode, @Maker, @Model, @Variant_1, @Variant_2, @Operator, @Production_Date, @Line, @Lot_No, @Jig_No, @Channel, @Spec_UL, @Spec_LL, @Leak_Value, @Result);
+```
+
+### 2.3 소스코드 구현 핵심 사항
+* **[EolDatabaseService.cs](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Services/EolDatabaseService.cs#L24-L101)**:
+ * WPF 애플리케이션의 전역 로그인 상태 개체인 `App.LoginResult`를 안전하게 로컬 변수로 바인딩(`Null-coalescing` 연산자 적용)하여 로그인 정보가 비어 있는 비정상 상황에서도 DB 저장이 실패하지 않도록 설계했습니다.
+ * `decimal.TryParse` 패턴을 도입하여 `SpecUL`, `SpecLL`, `MeasuredValue`와 같은 수치형 데이터를 안전하게 파싱한 뒤 SQL Parameter로 전달함으로써 타입 변환 예외 위험을 격리했습니다.
+ * **개별 매핑 및 시간 동기화**: `inspectData.IcSn` 및 `inspectData.PcbBarcode`를 DB 파라미터로 각각 매핑하고, 저장 시간 역시 CSV와의 정합성 일치를 위해 `inspectData.ProductionDate ?? DateTime.Now`를 바인딩하도록 고도화했습니다.
+* **[TestProcessService.cs (EolDatabaseService 인스턴스 생성)](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Services/TestProcessService.cs)**:
+ * 기존에 분산 관리되던 DB 접속 스트링(`config.xml`의 `DbConnectionString`) 대신, 모든 데이터베이스 접속 및 검증 주소를 `Database.ini` 파일 하나로 일원화하기 위해 `DatabaseSettings.Load(dbIniPath).ConnectionString` 방식으로 접속 정보를 읽어오도록 수정하여 설정 관리 포인트를 통합했습니다.
+ * **메타데이터 저장 정합성 확보**: 합격, 불합격, 타임아웃 등 모든 분기에서 `InspectData` 객체 생성 시 로그인 정보(`Maker`, `Model`, `Variant`, `Operator` 등) 및 실제 검사 시간(`ProductionDate`), 바코드 값을 완벽히 주입하여 로컬 CSV 백업 파일 작성 시 공란으로 비어있지 않도록 일원화했습니다.
+* **[ProjectModels.cs (AppConfig 및 InspectData)](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Models/ProjectModels.cs#L19-L21)**:
+ * `AppConfig` 내부에 존재하던 데이터베이스 연결 주소 하드코딩 속성에 `[Obsolete]` 경고를 지정하고, 실제 데이터베이스 연결에서는 이 속성이 사용되지 않도록 완전히 배제했습니다.
+ * `InspectData` 클래스 모델을 확장하여 새로운 DB 테이블 컬럼 바인딩 필드(Maker, Model, Variant 등)를 추가하여 데이터 저장 및 표현의 정합성을 수립했습니다.
+* **[LogUtils.cs (FileLogger 및 LogParser)](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Utils/LogUtils.cs)**:
+ * **로컬 CSV 16컬럼 저장**: `FileLogger.LogInspectData`를 수정하여 DB `dbo.EOL` 테이블 스키마와 완벽히 동일한 16개 컬럼(`IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,Lot_No,Jig_No,Channel,Spec_UL,Spec_LL,Leak_Value,Result`) 순서 및 헤더로 CSV 로그 파일에 기록하도록 개선했습니다.
+ * **상세 필터 검색**: `LogParser.ParseLogs` 메서드 시그니처를 확장하여 `opFilter`, `lotFilter`, `modelFilter` 매개변수를 추가하고, DB 쿼리 실행 시 `LIKE` 조건식을 동적으로 조합해 교차 상세 검색을 수행하도록 수정했습니다.
+ * **안정성 및 공유 잠금(FileShare) 조치**: 현장 작업자가 엑셀로 백업 CSV를 열어 두어 파일 락이 발생하더라도 `FileShare.ReadWrite` 모드를 적용하여 데이터가 안전하게 읽어지도록 개선했습니다.
+ * **로캘 독립적 날짜 파싱**: 시스템 국가 언어 설정과 관계없이 안전하게 파일을 파싱할 수 있게 `DateTime.TryParseExact`로 날짜 포커스를 통제했습니다.
+ * **중복 제거 필터 적용**: 동일 제품 재검사 시 DB는 덮어쓰기되고 CSV는 누적되는 동작 차이를 극복하기 위해, CSV 조회(`ParseLogsFromCsv`) 시 동일 제품 코드에 한해 가장 최신 날짜/시간의 1건만 노출하도록 중복 제거 필터를 적용했습니다.
+ * **CSV Fallback 및 하위 호환성**: DB 연결 실패 혹은 미등록 시 작동하는 로컬 CSV 파싱(`ParseLogsFromCsv`) 과정에서, 한 행의 쉼표 분할 크기(`parts.Length`)를 기반으로 구형 8개 컬럼 파일과 신규 16개 컬럼 파일을 자동 감지해 맵핑하는 정합성 및 예외 안전 장치를 탑재했습니다. 필터 조건 입력 시 구형 포맷은 자동 필터링 제외 처리됩니다.
+* **[DataViewModel.cs](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/ViewModels/DataViewModel.cs)**:
+ * `OperatorFilter`, `LotNoFilter`, `ModelFilter` 바인딩 속성을 신설하고, 검색 실행(`ExecuteSearch`) 호출 시 이를 `LogParser.ParseLogs`에 매개변수로 전달하여 연계 검색이 수행되도록 변경했습니다.
+* **[DataView.xaml](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Views/DataView.xaml)**:
+ * 확장된 상세 필터 조건들을 유연하게 담고 레이아웃이 훼손되지 않도록 필터 패널 영역을 **2행(Rows) 구조**로 전면 리디자인하여 UX 사용성을 개선했습니다.
+ * 데이터 그리드의 열 바인딩 정보를 신규 스키마 컬럼 구성과 일치하도록 17개 컬럼으로 보강했습니다.
+
+---
+
+## 3. 개발 및 빌드 검증 가이드
+
+### 3.1 Non-SDK 스타일 빌드 경고 및 주의사항
+본 프로젝트는 **Non-SDK 스타일(C# .NET Framework 4.7.2)** 기반의 레거시 WPF 애플리케이션입니다.
+* **문제 현상**: `dotnet build`를 사용해 컴파일을 시도하면 XAML 파일을 파싱 및 코드비하인드로 자동 변환하는 WPF 전용 MSBuild Target 프로세스가 누락되어 `g.cs` 임시 파일들이 누락됩니다. 이로 인해 컨트롤 식별자가 정의되지 않았다는 다수의 컴파일 에러(예: `CS0103 'Variant1TextBox' 이름이 현재 컨텍스트에 없습니다.`)가 발생하게 됩니다.
+* **올바른 빌드 방법**: Visual Studio 2022 환경의 정식 `MSBuild.exe`를 사용하여 컴파일을 수행해야 합니다.
+ ```powershell
+ # MSBuild 정식 컴파일 명령어 (Cwd: leak_test_project/)
+ & "C:\Program Files\Microsoft Visual Studio\18\Insiders\MSBuild\Current\Bin\MSBuild.exe" "leak_test_project\leak_test_project.csproj" /t:Rebuild /p:Configuration=Release
+ ```
+
+### 3.2 빌드 배포 자동화 (`.csproj`)
+`Database.ini` 파일이 프로그램 실행 및 릴리즈 배포 경로에 자동 포함되도록 하기 위해 `leak_test_project.csproj` 파일 내에 아래와 같이 `PreserveNewest` 복사 수명 설정을 구현했습니다.
+```xml
+
+
+
+ PreserveNewest
+
+
+```
+이를 통해 VS에서 프로젝트를 빌드하거나 재빌드할 때마다 자동으로 최신 `Database.ini` 파일이 컴파일 결과물 폴더(`bin/Debug/` 및 `bin/Release/`)로 자동 갱신 및 복사됩니다.