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 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - -