Browse Source

아이콘 추가

main
최승민 2 weeks ago
parent
commit
da0f99a83d
  1. 4
      leak_test_project.Setup/Bundle.wxs
  2. 46
      leak_test_project.Setup/Product.wxs
  3. 2
      leak_test_project/App.xaml.cs
  4. BIN
      leak_test_project/EOL.ico
  5. 3
      leak_test_project/MainWindow.xaml
  6. 6
      leak_test_project/Manual/01_시스템_개요.md
  7. 12
      leak_test_project/Manual/02_초기_설정.md
  8. 16
      leak_test_project/Manual/03_메인_화면.md
  9. 17
      leak_test_project/Manual/04_통신_설정.md
  10. 86
      leak_test_project/Manual/06_데이터_조회.md
  11. 141
      leak_test_project/Manual/07_시험_프로세스.md
  12. 79
      leak_test_project/Manual/08_로그_관리.md
  13. 20
      leak_test_project/Manual/09_오류_대응.md
  14. 550
      leak_test_project/Manual/Manual_Merged.html
  15. 438
      leak_test_project/Manual/Manual_Merged.md
  16. 4
      leak_test_project/Manual/사용자_메뉴얼.md
  17. 5
      leak_test_project/Models/ProjectModels.cs
  18. 2
      leak_test_project/Properties/PublishProfiles/ClickOnceProfile.pubxml
  19. 4
      leak_test_project/Properties/PublishProfiles/FolderProfile.pubxml
  20. 2
      leak_test_project/Properties/PublishProfiles/FolderProfile.pubxml.user
  21. 89
      leak_test_project/Services/Board4251.cs
  22. 107
      leak_test_project/Services/SentinelC28Service.cs
  23. 507
      leak_test_project/Services/TestProcessService.cs
  24. 309
      leak_test_project/Utils/LogUtils.cs
  25. 1
      leak_test_project/ViewModels/DataViewModel.cs
  26. 15
      leak_test_project/ViewModels/HomeViewModel.cs
  27. 14
      leak_test_project/Views/CommunicationWindow.xaml
  28. 6
      leak_test_project/Views/CommunicationWindow.xaml.cs
  29. 3
      leak_test_project/Views/StartupLoginWindow.xaml
  30. 130
      leak_test_project/leak_test_project.csproj
  31. 60
      ob/05_Implementation_History.md
  32. 57
      ob/06_Developer_Manual.md

4
leak_test_project.Setup/Bundle.wxs

@ -1,5 +1,5 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
<Bundle Name="Leak Test System" Version="1.0.0" Manufacturer="MobiDigm" UpgradeCode="d2c8d0f1-4df2-4752-9b24-7f154562c5b4">
<Bundle Name="Leak Test System" Version="1.0.0" Manufacturer="MobiDigm" UpgradeCode="d2c8d0f1-4df2-4752-9b24-7f154562c5b4" IconSourceFile="..\leak_test_project\EOL.ico">
<BootstrapperApplication>
<bal:WixStandardBootstrapperApplication LicenseUrl="https://www.mobidigm.co.kr" Theme="hyperlinkLicense" SuppressOptionsUI="yes" />
@ -7,7 +7,7 @@
<Chain>
<!-- 빌드 프로세스 하위에 생성된 MSI 패키지를 래핑 -->
<MsiPackage SourceFile="C:\LeakTest\LeakTestSetup.msi" Compressed="yes" Vital="yes" />
<MsiPackage SourceFile="$(var.MsiPath)" Compressed="yes" Vital="yes" />
</Chain>
</Bundle>
</Wix>

46
leak_test_project.Setup/Product.wxs

@ -8,46 +8,74 @@
<!-- 바탕화면 바로가기를 위한 DesktopFolder 참조용 표준 폴더 선언 -->
<StandardDirectory Id="DesktopFolder" />
<!-- C 드라이브 루트 하위에 LeakTest 폴더 생성 강제 고정 -->
<!-- C 드라이브 루트 하위에 LeakTest 폴더 생성 강제 고정 (설치 시 목표 경로) -->
<StandardDirectory Id="TARGETDIR" />
<SetDirectory Id="LeakTestFolder" Value="C:\LeakTest" />
<!-- C:\ProgramData 참조용 표준 폴더 선언 -->
<StandardDirectory Id="CommonAppDataFolder">
<Directory Id="ProgramDataLeakTestFolder" Name="LeakTest" />
</StandardDirectory>
<!-- 제어판 프로그램 목록용 아이콘 설정 -->
<Icon Id="AppIcon.ico" SourceFile="$(var.PublishSourceDir)\EOL.ico" />
<Property Id="ARPPRODUCTICON" Value="AppIcon.ico" />
<Directory Id="LeakTestFolder">
<!-- libs 폴더 제거: DllImport는 EXE 실행 폴더에서 검색하므로 루트에 DLL 배치 -->
<!-- Logs 폴더 서브 디렉토리 -->
<Directory Id="LogsFolder" Name="Logs" />
</Directory>
<ComponentGroup Id="ProductComponents" Directory="LeakTestFolder">
<!-- 실행 파일 -->
<Component Id="MainExecutable" Guid="f128c312-d922-4824-811c-d760773d2aae">
<File Id="leak_test_project.exe" Source="C:\LeakTest\leak_test_project.exe" KeyPath="yes">
<File Id="leak_test_project.exe" Source="$(var.PublishSourceDir)\leak_test_project.exe" KeyPath="yes">
<!-- 바탕화면에 바로가기 생성 -->
<Shortcut Id="DesktopShortcut" Directory="DesktopFolder" Name="Leak Test System" WorkingDirectory="LeakTestFolder" Advertise="no" />
<Shortcut Id="DesktopShortcut" Directory="DesktopFolder" Name="Leak Test System" WorkingDirectory="LeakTestFolder" Advertise="no" Icon="AppIcon.ico" />
</File>
</Component>
<!-- 구성 파일 -->
<Component Id="ConfigFile" Guid="a76d8b9e-64bc-44d5-9118-2e02b781da22">
<File Id="leak_test_project.dll.config" Source="C:\LeakTest\leak_test_project.dll.config" KeyPath="yes" />
<File Id="leak_test_project.dll.config" Source="$(var.PublishSourceDir)\leak_test_project.dll.config" KeyPath="yes" />
</Component>
<!-- 아이콘 파일 -->
<Component Id="EolIco" Guid="f31fa122-8321-4f92-9c12-32a76f2bfaee">
<File Id="EOL.ico" Source="$(var.PublishSourceDir)\EOL.ico" KeyPath="yes" />
</Component>
<!-- DB 및 로그인 설정 파일 -->
<Component Id="DatabaseIni" Guid="d82b4a1b-32ba-4b2a-a924-f3c14a2bb3ee">
<File Id="Database.ini" Source="C:\LeakTest\Database.ini" KeyPath="yes" />
<File Id="Database.ini" Source="$(var.PublishSourceDir)\Database.ini" KeyPath="yes" />
</Component>
<Component Id="LoginOptionsJson" Guid="c1a2d3e4-f5f6-4a4b-8c8d-9e9f0a0b0c0d">
<File Id="LoginOptions.json" Source="C:\LeakTest\LoginOptions.json" KeyPath="yes" />
<File Id="LoginOptions.json" Source="$(var.PublishSourceDir)\LoginOptions.json" KeyPath="yes" />
</Component>
<!-- PCI-Dask.dll: [DllImport("PCI-Dask.dll")] 탐색 경로 = EXE 실행 폴더(루트)에 배치 필수 -->
<Component Id="PciDaskDll" Guid="b129a28c-c2df-424a-8bf8-d61b12b23a9a">
<File Id="PCI_Dask.dll" Source="C:\LeakTest\PCI-Dask.dll" KeyPath="yes" />
<File Id="PCI_Dask.dll" Source="$(var.PublishSourceDir)\PCI-Dask.dll" KeyPath="yes" />
</Component>
<!-- Microsoft.Data.SqlClient.SNI.dll도 동일하게 루트에 배치 -->
<Component Id="SqlSniDll" Guid="c29a828d-d34e-42ef-abf9-c72b23c24b9b">
<File Id="Microsoft.Data.SqlClient.SNI.dll" Source="C:\LeakTest\Microsoft.Data.SqlClient.SNI.dll" KeyPath="yes" />
<File Id="Microsoft.Data.SqlClient.SNI.dll" Source="$(var.PublishSourceDir)\Microsoft.Data.SqlClient.SNI.dll" KeyPath="yes" />
</Component>
<!-- C:\LeakTest 루트 파일 제거 (Logs 폴더 및 로그 데이터는 보존) -->
<Component Id="CleanupLeakTestRoot" Guid="a128c312-d922-4824-811c-d760773d2aaf">
<CreateFolder />
<RemoveFile Id="PurgeRootFiles" On="uninstall" Name="*" Directory="LeakTestFolder" />
<RemoveFolder Id="CleanupRootFolder" On="uninstall" Directory="LeakTestFolder" />
</Component>
<!-- C:\ProgramData\LeakTest 폴더 및 하위 설정 파일 삭제 -->
<Component Id="CleanupProgramData" Guid="e228c312-d922-4824-811c-d760773d2aaf" Directory="ProgramDataLeakTestFolder">
<CreateFolder />
<RemoveFile Id="PurgeProgramDataFiles" On="uninstall" Name="*" />
<RemoveFolder Id="CleanupProgramDataFolder" On="uninstall" />
</Component>
</ComponentGroup>

2
leak_test_project/App.xaml.cs

@ -27,7 +27,7 @@ namespace leak_test_project
{
string defaultContent =
"; =====================================================================\r\n" +
"; Mobi Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)\r\n" +
"; Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)\r\n" +
"; =====================================================================\r\n" +
"\r\n" +
"; [데이터베이스 접속 상세 설정]\r\n" +

BIN
leak_test_project/EOL.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

3
leak_test_project/MainWindow.xaml

@ -5,7 +5,8 @@
Title="Pressure Leak Inspect System" Height="800" Width="1024"
MinWidth="1150" MinHeight="700"
WindowState="Maximized" Background="#F0F0F0" WindowStyle="None"
ContentRendered="Window_ContentRendered">
ContentRendered="Window_ContentRendered"
Icon="EOL.ico">
<Window.DataContext>
<viewmodels:MainViewModel/>

6
leak_test_project/Manual/01_시스템_개요.md

@ -6,7 +6,7 @@
## 1.1 시스템 개요
**Pressure Leak Inspect System**은 제품의 압력 누설(Leak)을 자동으로 검사하는 WPF 기반 데스크탑 애플리케이션입니다. 생산 라인에서 DIO 시작 신호를 받아 제품 ID를 판독하고, 누설 시험을 수행한 뒤 합격/불합격을 자동 판정합니다.
**Pressure Leak Inspect System**은 제품의 압력 누설(Leak)을 자동으로 검사하는 데스크탑 프로그램입니다. 생산 라인에서 DIO 시작 신호를 받아 제품 ID를 판독하고, 누설 시험을 수행한 뒤 합격/불합격을 자동 판정합니다.
### 주요 기능 요약
@ -19,7 +19,7 @@
| **PCI-7432 DIO 보드** | 디지털 입출력 제어 (시작 신호 수신, OK/NG 출력) |
| **검사 기록 관리** | 날짜/판정/ID별 필터링 검색 및 CSV 파일 내보내기를 지원합니다 |
| **SPEC 교차 검증** | 프로그램 판정과 센서 자체 판정을 비교하여 불일치 시 경고합니다 |
| **순차 실행 보장** | 세마포어 잠금으로 좌/우 채널 동시 시험을 방지합니다 |
| **순차 실행 보장** | 중복 실행 방지 기능으로 좌/우 채널의 동시 시험을 방지합니다 |
---
@ -28,7 +28,7 @@
| 항목 | 요구사항 | 비고 |
|------|----------|------|
| **운영체제** | Windows 10 이상 (64bit 권장) | 32bit도 지원 가능 |
| **.NET 런타임** | .NET 9 (net9.0-windows) | 별도 설치 필요 없음 (Self-Contained 배포) |
| **.NET 런타임** | .NET 9 | 별도 설치 필요 없음 (자체 포함 배포) |
| **드라이버** | ADLINK PCIS-DASK 드라이버 | `PCI-Dask.dll` 필요 |
| **시리얼 포트** | 최소 2개의 COM 포트 | C28 센서용 + 4251 보드용 |
| **화면 해상도** | 1024×768 이상 | 최대화 실행 시 1920×1080 권장 |

12
leak_test_project/Manual/02_초기_설정.md

@ -23,7 +23,7 @@
## 2.2 프로그램 실행
### 실행 방법
1. `leak_test_project.exe` 파일을 **더블 클릭**하여 실행합니다.
1. 바탕화면의 **바로가기 아이콘**을 더블 클릭하여 실행합니다.
2. 프로그램이 전체화면(Maximized) 모드로 시작됩니다.
### 최초 실행 시 자동 생성되는 파일
@ -113,10 +113,10 @@
```ini
; [데이터베이스 접속 상세 설정]
Db_Server=192.168.0.20
Db_Name=NE1aW_PT_SENSOR
Db_User=sa
Db_Password=sa1234
Db_Server=[데이터베이스_서버_IP],[포트번호]
Db_Name=[데이터베이스_이름]
Db_User=[접속_계정_ID]
Db_Password=[접속_비밀번호]
[Login]
Procedure=dbo.CheckOperator
@ -124,7 +124,7 @@ ProcessName=EOL
OfflinePreview=false
```
- **온라인 인증 모드 (`OfflinePreview=false`)**: 지정된 개별 데이터베이스 접속 정보(`Db_Server`, `Db_Name`, `Db_User`, `Db_Password`)를 기반으로 데이터베이스에 접속하여 Stored Procedure(`Procedure`)로 ID/PW 존재 여부를 직접 확인합니다. DB 서버 연결 실패나 미등록 사용자일 경우 로그인이 거부됩니다.
- **온라인 인증 모드 (`OfflinePreview=false`)**: 지정된 데이터베이스 접속 정보(`Db_Server`, `Db_Name`, `Db_User`, `Db_Password`)를 기반으로 데이터베이스에 접속하여 사용자 계정의 유효성을 검증합니다. DB 서버 연결 실패나 미등록 사용자일 경우 로그인이 거부됩니다.
- **오프라인 테스트 모드 (`OfflinePreview=true`)**: 데이터베이스를 거치지 않고, 필수 입력 값을 채우면 즉시 로그인이 허용됩니다. 네트워크 연결이 안 된 시뮬레이션 및 테스트 환경에서 사용합니다.

16
leak_test_project/Manual/03_메인_화면.md

@ -48,22 +48,22 @@
- **LEFT LEAK TESTER** 또는 **RIGHT LEAK TESTER**
- 짙은 남색 배경에 백색 볼드 텍스트로 채널 구분을 명시합니다.
#### ② 시작 시간 (Row 1)
#### ② 시작/종료 시간 (Row 1)
| 항목 | 설명 |
|------|------|
| 라벨 | `시작 시간` (회색 헤더 배경) |
| 값 | `yyyy-MM-dd HH:mm:ss` 형식 (예: `2026-05-08 10:30:15`) |
| 갱신 시점 | 시험이 시작될 때마다 현재 시각으로 자동 갱신 |
| 라벨 | `시작/종료` (회색 헤더 배경) |
| 값 | `시작시간 / 종료시간` 형식 (예: `2026-05-08 10:30:15 / -` 또는 `2026-05-08 10:30:15 / 10:30:45`) |
| 갱신 시점 | 시험이 시작될 때 `시작시간 / -` 형태로 갱신되며, 시험 완료(DB 저장) 시점에 종료 시간이 함께 기록됩니다. |
#### ③ IC_SN / PCB_Barcode (Row 2~3) - **식별자 2행 분리 표시**
| 항목 | 설명 |
|------|------|
| 라벨 | `IC_SN` / `PCB_Barcode` (각각 회색 헤더 배경, 개별 행으로 표시) |
| 값 | 4251 보드를 통해 판독한 제품 고유 시리얼 번호 |
| 데이터 소스 | 4251 보드를 통해 제품 ID 센서에서 읽은 값 |
| 갱신 시점 | 시험 시작 시 초기화, 센서 판독 완료 시 표시 |
| 값 | 각각 제품의 IC 일련번호 및 이에 매핑되는 PCB 바코드 고유 식별 정보 |
| 데이터 소스 | **IC_SN**: 4251 보드로 제품 ID 센서에서 판독한 값<br>**PCB_Barcode**: 판독한 `IC_SN`을 기준으로 데이터베이스에서 조회한 바코드 값 (오프라인 모드 시 임시 데이터 주입) |
| 갱신 시점 | 시험 시작 시 빈 값(`-`)으로 초기화, 판독 및 조회 완료 시 표시 |
> **참고**: 가로 텍스트 잘림 방지 및 가독성 강화를 위해 IC_SN과 PCB_Barcode를 별도 행으로 분리하여 표시합니다.
> **참고**: 텍스트 잘림 방지 및 가독성 향상을 위해 IC_SN과 PCB_Barcode를 별도의 행으로 나누어 표시합니다. 프로그램 창 크기를 소형으로 조절하더라도 텍스트가 잘리지 않고 정상 표시됩니다.
#### ④ 측정값 + SPEC (Row 3) - **가장 큰 영역**
좌측에 측정값, 우측에 SPEC 규격이 함께 표시됩니다.

17
leak_test_project/Manual/04_통신_설정.md

@ -28,16 +28,27 @@
| **Sensor BaudRate** | 드롭다운 | 9600 / 19200 / 38400 / 115200 | `9600` | C28 센서 통신 속도. 센서 설정과 일치해야 함 |
| **Sensor Port** | 드롭다운 | 시스템 감지 COM 포트 목록 | `COM1` | C28 센서가 연결된 시리얼 포트 |
## 4.4 버튼 설명
## 4.4 스펙 불일치 알림 설정 (화면 하단 영역)
| 항목 | UI 타입 | 선택 가능한 값 | 기본값 | 설명 |
|------|---------|---------------|--------|------|
| **스펙 불일치 알림 활성화** | 체크박스 | 체크(ON) / 체크 해제(OFF) | `체크(ON)` | 프로그램 판정과 센서 자체 판정이 맞지 않을 때의 알림 팝업 창 발생 여부 설정 |
> **동작 설명**:
> - **ON (기본값)**: 판정이 불일치할 때 메인 화면의 오류 메시지 창에 로그가 기재됨과 동시에, 경고 대화상자("프로그램 스펙과 센서의 스펙이 서로 맞지 않습니다.") 팝업이 출력되어 작업자 인지를 강제합니다.
> - **OFF**: 판정이 불일치하더라도 경고 팝업을 띄우지 않고, 화면의 오류 메시지 영역에만 텍스트로 기록을 남깁니다. (콘솔 로그 기록 등은 유지)
> - 이 설정은 `config.xml` 내의 `EnableSpecMismatchAlarm` 옵션에 영속 저장되어 프로그램 재작동 시에도 마지막 상태가 자동 유지됩니다.
## 4.5 버튼 설명
| 버튼 | 특징 | 동작 |
|------|------|------|
| **설정 저장** | 녹색 계열 | config.xml에 저장 → 기존 연결 해제 → 새 설정으로 재연결 → "통신 설정이 저장되었습니다." 메시지 → 창 닫힘 |
| **닫기** | 회색 계열 | 변경 사항 저장하지 않고 창 닫기 |
## 4.5 저장 시 내부 동작 순서
## 4.6 저장 시 내부 동작 순서
1. 현재 선택된 값을 `config.xml`에 저장
1. 현재 선택된 값을 `config.xml`에 저장 (스펙 불일치 알림 활성화 여부 포함)
2. 기존 Sentinel C28 시리얼 연결 해제
3. 기존 4251 보드 시리얼 연결 해제
4. 시험 프로세스 스레드 종료

86
leak_test_project/Manual/06_데이터_조회.md

@ -14,68 +14,68 @@
| 항목 | UI 타입 | 기본값 | 설명 |
|------|---------|--------|------|
| **조회 기간** | DatePicker (달력) x 2 | 오늘로부터 7일 전 ~ 오늘 | 조회 대상 날짜 범위 설정 (달력 확대 표시 적용) |
| **판정 결과** | 드롭다운 ComboBox | `[전체]` / `OK` / `NG` | 판정 결과에 따른 필터링 (기본값: 전체) |
| **조회 기간** | 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`) 컬럼에 대한 부분 일치 검색 | (빈 값 = 필터 제외) |
| **ID 검색** | TextBox (텍스트 입력) | `IC_SN` 또는 `PCB_Barcode` 열에 대한 부분 일치 (LIKE) 검색을 수행합니다. | (빈 값 = 필터 제외) |
| **작업자** | TextBox (텍스트 입력) | 작업자 ID (`Operator`) 열에 대한 부분 일치 검색을 수행합니다. | (빈 값 = 필터 제외) |
| **Lot No** | TextBox (텍스트 입력) | 로트 번호 (`Lot_No`) 열에 대한 부분 일치 검색을 수행합니다. | (빈 값 = 필터 제외) |
| **모델** | TextBox (텍스트 입력) | 모델명 (`Model`) 열에 대한 부분 일치 검색을 수행합니다. | (빈 값 = 필터 제외) |
## 6.3 조회 및 취소 버튼
| 버튼 | 색상 | 동작 |
|------|------|------|
| **Search** | 파란색 계열 | 설정된 2행의 모든 필터 조건으로 로컬 로그 CSV 백업 파일을 부분일치 검색하여 그리드에 표시합니다. (안정성을 위해 DB 접속 조회가 아닌 로컬 파일 조회로 작동합니다.) |
| **Cancel (취소)** | 빨간색 계열 | 조회 중일 때 로딩 화면 내부에 나타나며, 진행 중인 파일 검색 작업을 즉시 중단하고 화면을 복귀시킵니다. |
| **Search** | 파란색 계열 | 설정된 필터 조건으로 로컬 로그 CSV 백업 파일을 검색하여 그리드에 표시합니다. (시스템 안정성을 위해 데이터베이스 직접 조회 대신 로컬 백업 파일을 조회하도록 설계되었습니다.) |
| **Cancel (취소)** | 빨간색 계열 | 조회 중일 때 로딩 화면 내부에 나타나며, 진행 중인 파일 검색 작업을 중단하고 이전 화면으로 복귀합니다. |
### Search 버튼 동작 상세
1. 설정된 조회 기간 날짜를 직접 일자별로 조합하여 해당하는 일일 로그 CSV 파일만 콕 집어 정밀 탐색합니다. (전체 디렉토리 풀 스캔 차단으로 조회 속도가 매우 빠릅니다.)
2. 검색 조건(판정, ID, 작업자, Lot No, 모델)에 맞는 대상을 인메모리 부분일치 검색 처리합니다. (과거 구형 8개 컬럼 포맷 파일도 하위 호환되어 정상 파싱됩니다.)
3. 검색 결과가 없으면 "해당 조건의 데이터가 없습니다." 알림 표시.
1. 지정된 조회 기간에 해당하는 일별 로그 CSV 파일만 조회하여 검색을 수행합니다. (불필요한 파일 스캔을 제한하여 검색 속도를 향상시켰습니다.)
2. 검색 조건(판정, ID, 작업자, Lot No, 모델)에 맞는 데이터를 인메모리 부분 일치 방식으로 검색합니다.
3. 검색 결과가 존재하지 않을 경우 "해당 조건의 데이터가 없습니다." 알림 표시합니다.
### 조회 중 로딩 및 Cancel 버튼 동작 상세
1. Search 버튼 클릭 시 화면 전체에 "Searching Logs..." 라는 반투명 어두운 로딩 오버레이가 활성화되어 작업 중임을 시각적으로 나타냅니다.
2. 데이터가 많아 조회가 오래 걸릴 경우 로딩 오버레이의 **Cancel (취소)** 버튼을 누르면 즉시 조회가 중단되고, "조회가 취소되었습니다." 알림과 함께 메인 화면으로 안전하게 복귀합니다.
## 6.4 데이터 그리드 컬럼 (DB EOL 스키마 표준 규격)
| 컬럼 헤더명 | 바인딩 프로퍼티 | 정렬 | 설명 |
|------|------|------|------|
| **IC_SN** | `IcSn` | 좌측 | 제품 고유 IC SN 시리얼 번호 (PK/FK) |
| **PCB_Barcode** | `PcbBarcode` | 좌측 | PCB 바코드 고유 식별 정보 (UK) |
| **Maker** | `Maker` | 좌측 | 로그인 시 입력된 제조사명 |
| **Model** | `Model` | 좌측 | 로그인 시 입력된 검사 모델명 |
| **Variant_1** | `Variant1` | 좌측 | 로그인 시 입력된 세부 파생 모델 1 |
| **Variant_2** | `Variant2` | 좌측 | 로그인 시 입력된 세부 파생 모델 2 |
| **Operator** | `Operator` | 좌측 | 작업 진행자 사용자 ID |
| **Production_Date** | `ProductionDate` | 좌측 | 검사 수행 및 완료 일시 (yyyy-MM-dd HH:mm:ss) |
| **Line** | `Line` | 좌측 | 로그인 공정 작업 라인 |
| **Lot_No** | `LotNo` | 좌측 | 생산 Lot 번호 |
| **Jig_No** | `JigNo` | 좌측 | 사용 지그(Jig) 번호 |
| **Channel** | `Channel` | 좌측 | 검사 채널 (`LEFT` 또는 `RIGHT`) |
| **Spec_UL** | `SpecUL` | 좌측 | 누설 상한 규격값 (sccm) |
| **Spec_LL** | `SpecLL` | 좌측 | 누설 하한 규격값 (sccm) |
| **Leak_Value** | `MeasuredValue` | **우측** | 측정된 누설 리크량 (**볼드 표시**) |
| **Result** | `Judgment` | **중앙** | 최종 판정 결과 (`OK` 초록 / `NG` 빨강 볼드 강조) |
### 그리드 스타일
- 격자선: 수평/수직 연회색 격자선 표시
- 홀수 행 배경: 흰색, 짝수 행 배경: 연회색 배경으로 행 식별 편의 제공
- 선택된 행: 연파랑 배경에 파란색 텍스트로 강조
- 컬럼 헤더 클릭 시 해당 열 기준 오름차순/내림차순 정렬 가능 (컬럼 순서 변경 불가)
- 최소 50행이 항상 표시됩니다 (데이터가 적어도 격자 형태 유지를 위해 빈 행으로 채워짐)
1. Search 버튼 클릭 시 화면 전체에 "Searching Logs..." 라는 반투명 오버레이가 활성화되어 검색이 진행 중임을 나타냅니다.
2. 검색이 지연될 경우 로딩 오버레이의 **Cancel (취소)** 버튼을 누르면 즉시 조회가 중단되고, "조회가 취소되었습니다." 메시지 박스 팝업 후 자동으로 메인 화면으로 복귀합니다.
## 6.4 데이터 조회 결과 표(그리드) 항목 안내
| 컬럼 헤더명 | 정렬 | 설명 |
|------|------|------|
| **IC_SN** | 좌측 | 제품 고유 IC 시리얼 번호 |
| **PCB_Barcode** | 좌측 | PCB 바코드 고유 식별 정보 |
| **Maker** | 좌측 | 로그인 시 입력된 제조사명 |
| **Model** | 좌측 | 로그인 시 입력된 검사 모델명 |
| **Variant_1** | 좌측 | 로그인 시 입력된 세부 파생 모델 1 |
| **Variant_2** | 좌측 | 로그인 시 입력된 세부 파생 모델 2 |
| **Operator** | 좌측 | 작업 진행자 사용자 ID |
| **Production_Date** | 좌측 | 검사 수행 및 완료 일시 (yyyy-MM-dd HH:mm:ss) |
| **Line** | 좌측 | 로그인 공정 작업 라인 |
| **Lot_No** | 좌측 | 생산 Lot 번호 |
| **Jig_No** | 좌측 | 사용 지그(Jig) 번호 |
| **Channel** | 좌측 | 검사 채널 (`LEFT` 또는 `RIGHT`) |
| **Spec_UL** | 좌측 | 누설 상한 규격값 (sccm) |
| **Spec_LL** | 좌측 | 누설 하한 규격값 (sccm) |
| **Leak_Value** | **우측** | 측정된 누설 리크량 (**볼드 표시**) |
| **Result** | **중앙** | 최종 판정 결과 (`OK` 초록 / `NG` 빨강 볼드 강조) |
### 표(그리드) 스타일
- 격자선: 수평 및 수직 연회색 격자선을 표시합니다.
- 교대 행 배경: 홀수 행은 흰색, 짝수 행은 연회색 배경을 적용하여 가독성을 높였습니다.
- 선택된 행: 연파란색 배경에 파란색 텍스트로 강조합니다.
- 컬럼 헤더 정렬: 헤더 클릭 시 해당 열 기준으로 오름차순 또는 내림차순 정렬이 가능합니다. (컬럼 순서 변경은 제한됩니다.)
- 격자 유지: 조회된 데이터 개수가 50개 미만이더라도 화면 구성을 유지하기 위해 나머지 행은 빈 칸으로 채워져 표시됩니다.
## 6.5 화면 닫기
| 방법 | 설명 |
|------|------|
| 하단 **Close** 버튼 클릭 | 메인 화면으로 복귀 |
| 하단 메뉴 **Data** 버튼 재클릭 | 토글 동작으로 메인 화면으로 복귀 |
| 하단 **Close** 버튼 클릭 | 메인 화면으로 복귀합니다. |
| 하단 메뉴 **Data** 버튼 재클릭 | 토글 동작을 통해 메인 화면으로 복귀합니다. |
---

141
leak_test_project/Manual/07_시험_프로세스.md

@ -6,7 +6,7 @@
## 7.1 개요
자동 시험은 DIO 시작 신호(LEFT_START 또는 RIGHT_START)에 의해 트리거되며, 7단계로 진행됩니다. 좌/우 채널은 각각 독립적인 백그라운드 스레드에서 실행되지만, **세마포어 잠금**을 통해 동시 실행은 방지됩니다.
자동 시험은 시작 신호(LEFT_START 또는 RIGHT_START)에 의해 작동하며, 7단계로 진행됩니다. 좌/우 채널은 각각 독립적인 백그라운드 스레드에서 실행되지만, **중복 실행 방지 기능**을 통해 동시 시험은 방지됩니다.
## 7.2 전체 프로세스 흐름도
@ -14,16 +14,16 @@
<!-- Trigger -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<div style="font-weight: bold; color: #2f3542;">DIO 시작 신호 감지 <span style="font-weight: normal; font-size: 8pt; color: #747d8c;">(LEFT_START 또는 RIGHT_START: OFF➔ON)</span></div>
<div style="font-weight: bold; color: #2f3542;">시작 신호 감지 <span style="font-weight: normal; font-size: 8pt; color: #747d8c;">(좌측/우측 시작 신호 입력 시)</span></div>
</div>
<!-- Semaphore -->
<div style="margin-bottom: 20px; position: relative; background-color: #f1f2f6; padding: 10px; border-radius: 4px; border-left: 3px solid #f6b93b;">
<span style="position: absolute; left: -26px; top: 14px; width: 10px; height: 10px; background-color: #f6b93b; border-radius: 50%;"></span>
<strong>세마포어 잠금 대기</strong>
<strong>중복 실행 방지 대기</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
잠금 불가 시: "대기중" 메시지 표시 후 대기<br>
잠금 획득 시: 다음 단계로 진행 (좌/우 채널 동시 시험 방지)
다른 채널이 시험 중인 경우: "대기중" 메시지를 표시하며 순서 대기<br>
시험 가능 상태인 경우: 다음 단계 진행 (좌/우 채널 동시 시험 방지)
</div>
</div>
@ -32,59 +32,69 @@
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">1단계: 시험 시작 (초기화)</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
OK/NG DIO 출력 초기화 (OFF)<br>
합격/불합격 신호 출력 초기화 (OFF)<br>
• 메인 화면 결과 표시 초기화 및 시작 시간 갱신<br>
Sentinel C28 LEAK 센서 수신 이벤트 버퍼 초기화
누설 센서 수신 데이터 초기화
</div>
</div>
<!-- Step 2 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">2단계: 센서 정보 읽기 (ID 판독)</strong>
<strong style="color: #1e3799;">2단계: 제품 ID 판독</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 4251 보드를 통해 제품 ID 센서에서 16자리 ID 판독<br>
<span style="color: #ff4757; font-weight: bold;">[실패 시]</span> "ID 읽기 실패" 오류 출력 후 NG 판정 조기 종료 (DB 비동기 저장)
판독 장치(4251 보드)를 통해 제품 ID 센서에서 16자리 ID 판독<br>
<span style="color: #ff4757; font-weight: bold;">[실패 시]</span> "ID 읽기 실패" 오류 출력 후 시험이 종료되며 결과는 기록 및 저장에서 제외됩니다.
</div>
</div>
<!-- Step 3 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">3단계: LEAK 시험 수행 (계측)</strong>
<div style="margin-bottom: 15px; position: relative; background-color: #f8f9fa; padding: 10px; border-radius: 4px; border-left: 3px solid #1e3799;">
<span style="position: absolute; left: -26px; top: 14px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">3단계: PCB 바코드 데이터베이스 조회</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 화면에 "LEAK 시험중" 표시<br>
• Sentinel C28 센서의 최종 압력 누설 측정 결과 대기 (최대 30초)<br>
<span style="color: #ff4757; font-weight: bold;">[타임아웃]</span> "Sentinel C28 통신 타임아웃" 오류 후 NG 판정 종료
• 데이터베이스에서 제품 ID(`IC_SN`)에 매칭되는 `PCB_Barcode` 조회 (5초 대기 시간 초과 제한)<br>
<span style="color: #ff4757; font-weight: bold;">[실패/누락 시]</span> "바코드 매핑 실패" 오류 출력 후 즉시 종료되며 결과는 기록 및 저장에서 제외됩니다.
</div>
</div>
<!-- Step 4 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">4단계: 합격 판정</strong>
<strong style="color: #1e3799;">4단계: 누설 시험 수행 (계측)</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
<span style="font-family: monospace;">SPEC LL ≤ 측정값 ≤ SPEC UL</span> 이면 **OK (합격)**, 범위를 벗어나면 **NG (불합격)**
• 화면에 "LEAK 시험중" 표시<br>
• 누설 센서(Sentinel C28)로부터 측정 결과 수신 대기 (최대 30초)<br>
<span style="color: #ff4757; font-weight: bold;">[시간 초과 시]</span> 통신 시간 초과 오류 발생 및 불합격(NG) 판정으로 자동 종료 (측정값 9.9999로 데이터베이스에 저장)
</div>
</div>
<!-- Step 5 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">5단계: SPEC 교차 검증</strong>
<strong style="color: #1e3799;">5단계: 합격 판정</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 프로그램(UI) 판정과 센서 자체 판정(OK/A/P vs NG/R/F)을 상호 비교<br>
• 불일치 시 `SpecMismatch` 플래그 설정 및 경고 팝업 표시
• 규격 하한값 ≤ 측정값 ≤ 규격 상한값 이면 **OK (합격)**, 범위를 벗어나면 **NG (불합격)**로 판정합니다.
</div>
</div>
<!-- Step 6 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">6단계: 판정 스펙 교차 검증</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 프로그램 판정과 센서 자체 판정을 비교 분석합니다.<br>
• 판정이 서로 다를 경우 화면에 경고 창을 표시합니다. (통신 설정에서 알림 창 표시를 끌 수 있습니다.)
</div>
</div>
<!-- Step 7 -->
<div style="margin-bottom: 20px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">6단계: 결과 출력 및 저장</strong>
<strong style="color: #1e3799;">7단계: 결과 출력 및 저장</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 로컬 CSV 로그 파일 기록 및 MSSQL DB에 비동기 저장<br>
• 판정 결과에 따른 DIO 하드웨어 출력 (OK 또는 NG 포트 ON)<br>
• 로컬 CSV 백업 파일 기록 및 데이터베이스에 검사 결과 저장<br>
• 판정 결과에 따라 외부 설비에 출력 신호(OK 또는 NG 포트) 전송<br>
• 메인 화면에 최종 측정값 및 판정 출력 ("시험 완료" 표시)
</div>
</div>
@ -92,71 +102,80 @@
<!-- End -->
<div style="position: relative; background-color: #f1fff5; padding: 10px; border-radius: 4px; border-left: 3px solid #2ed573;">
<span style="position: absolute; left: -26px; top: 14px; width: 10px; height: 10px; background-color: #2ed573; border-radius: 50%;"></span>
<strong style="color: #2f855a;">세마포어 잠금 해제</strong>
<strong style="color: #2f855a;">순서 잠금 해제</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 시험 종료 후 잠금 해제 → 대기 중인 채널이 있을 경우 350ms 안정화 대기 후 자동 시작
• 시험 종료 후 순서 대기가 해제되며, 대기 중이던 다른 채널이 0.35초 대기 후 자동으로 시험을 개시합니다.
</div>
</div>
</div>
## 7.3 순차 실행 (세마포어 잠금)
## 7.3 좌/우 채널 순차 실행 및 대기
좌/우 채널은 **동시에 시험을 수행할 수 없습니다**. 이는 하드웨어 통신 충돌을 방지하기 위함입니다.
좌/우 채널은 **동시에 시험을 수행할 수 없습니다**. 이는 하드웨어 통신 간섭 및 충돌을 방지하기 위함입니다.
| 상황 | 동작 |
|------|------|
| 한 채널만 시작 신호 | 즉시 시험 시작 |
| 양쪽 동시 시작 신호 | 먼저 잠금을 획득한 채널이 시험 진행, 다른 채널은 "대기중" 표시 |
| 시험 완료 후 | 대기 중인 채널이 자동으로 시험 시작 (350ms 안정화 대기 후) |
| 한 채널만 시험 시작 신호 발생 | 즉시 시험을 시작합니다. |
| 양쪽 채널 동시 신호 발생 | 먼저 순서를 획득한 채널이 시험을 진행하며, 다른 채널은 "대기중"을 표시하며 대기합니다. |
| 시험 완료 후 | 대기 중이던 채널이 자동으로 시험을 시작합니다. (안정화를 위해 0.35초의 대기 시간이 적용됩니다.) |
> **참고**: 하드웨어 통신 안정화를 위해 이전 채널의 시험 종료 후 **0.35초의 여유 시간**이 자동으로 적용됩니다.
## 7.4 시간 초과 (타임아웃) 처리
누설 센서에서 30초 이내에 계측 결과 응답이 없으면 다음과 같이 처리됩니다.
1. 화면의 오류 메시지 영역에 "통신 시간 초과" 오류 메시지를 표시합니다. (데이터베이스 저장 실패 시 "DB 저장 실패" 사유도 함께 노출)
2. 해당 채널의 불합격(NG) 물리 신호를 내보냅니다.
3. 측정값 `9.9999`, 판정 `NG`로 기록을 생성합니다.
4. 로컬 CSV 로그 파일 및 데이터베이스에 검사 결과를 기록합니다. (단, 이전 단계인 제품 ID 판독에 성공한 상태여야 저장이 이루어집니다.)
> **참고**: 하드웨어 통신 안정화를 위해, 이전 채널 시험 종료 후 **350ms의 여유 시간**이 자동으로 적용됩니다.
## 7.5 제품 ID 및 PCB 바코드 연동 예외 처리
## 7.4 타임아웃 처리
데이터 일관성과 무결성을 확보하기 위해 제품 식별 정보가 불완전한 상태에서의 데이터 저장을 차단하는 안전 검증 절차가 작동합니다.
Sentinel C28 센서에서 30초 이내에 응답이 없으면:
### 1. 제품 ID(IC_SN) 판독 실패 시
- **동작**: 제품 ID 판독 장치(4251 보드)로부터 16자리 ID 취득을 실패하는 경우 진행 메시지에 `"ID 읽기 실패"`를 표시하고, 해당 채널의 NG 물리 신호를 즉시 출력합니다.
- **저장 제한**: 이 경우에는 로컬 CSV 로그 기록 및 데이터베이스 저장을 **완전히 건너뜁니다**.
1. "Sentinel C28 통신 타임아웃" 오류 메시지 표시 (DB 저장 실패 시 "DB 저장 실패"도 오류 메시지에 추가 표시)
2. 해당 채널 DIO `NG` 출력 ON
3. 측정값 `0.0000`, 판정 `NG`, 센서판정 `T/O` (Timeout)로 기록
4. CSV 로그 파일 및 MSSQL 데이터베이스에 NG 결과 비동기 기록
### 2. PCB 바코드 DB 조회(매핑) 실패 시
- **동작**: 판독 완료 후 데이터베이스 서버에서 `IC_SN`에 해당하는 `PCB_Barcode`를 조회할 때, 정해진 시간(5초) 내에 응답이 없거나 일치하는 데이터가 없는 경우 화면에 `"바코드 매핑 실패"` 관련 오류를 표시하고 NG 신호를 출력합니다.
- **저장 제한**: 이 경우에도 제품 검사 자체를 진행하지 않고 즉시 스킵하며, **데이터베이스 및 로컬 CSV 로그 기록 저장을 건너뜁니다**. (오프라인 모드에서는 임시 바코드 정보를 주입하여 검사 동작을 테스트할 수 있습니다.)
## 7.5 C28 센서 결과 채널 라우팅
## 7.6 누설 센서 측정 결과 채널 분류
Sentinel C28 센서에서 수신한 데이터는 채널 번호(ChannelNo)로 구분됩니다:
누설 센서에서 수신한 데이터는 다음 채널 번호 정보를 기준으로 정렬됩니다.
| 채널 번호 | 대상 |
|----------|------|
| `C01` 또는 `1` | LEFT 채널 |
| `C02` 또는 `2` | RIGHT 채널 |
| 채널 번호 없음 | 현재 활성화된(시험 중인) 채널로 자동 할당 |
| `C01` 또는 `1` | 좌측(LEFT) 채널 |
| `C02` 또는 `2` | 우측(RIGHT) 채널 |
| 채널 번호 식별 불가 | 현재 활성화되어 시험을 진행 중인 채널로 자동 배정 |
## 7.6 ID 수동 테스트
## 7.7 ID 수동 테스트
하단 메뉴의 **ID Test (L)** 또는 **ID Test (R)** 버튼으로 DIO 시작 신호 없이 센서 ID 판독만 수행할 수 있습니다.
하단 메뉴의 **ID Test (L)** 또는 **ID Test (R)** 버튼을 사용하면 시험 시작 신호 없이 센서 ID 판독 동작만 단독으로 수행할 수 있습니다.
### 실행 흐름
1. 세마포어 잠금 획득 (다른 채널 시험 중이면 대기)
2. 결과 표시 초기화
3. 500ms 대기 (UI 피드백용)
4. "센서 정보 읽는 중" 메시지 표시
5. 4251 보드를 통해 제품 ID 판독
6. 성공 시: ID 표시 + "ID 테스트 완료"
7. 실패 시: ID를 "-"로 표시 + "ID 테스트 실패"
8. 세마포어 잠금 해제
1. 중복 방지 잠금 획득 (다른 채널이 시험 중인 경우 대기)
2. 결과 표시 초기화 및 0.5초 대기
3. "센서 정보 읽는 중" 메시지 표시 후 ID 판독 시작
4. 성공 시: 판독된 ID 표시 및 "ID 테스트 완료" 메시지 표시
5. 실패 시: ID 영역에 "-" 표시 및 "ID 테스트 실패" 메시지 표시
6. 잠금 해제
> **용도**: 설치 후 하드웨어 연결 상태 확인, 디버깅 시 활용합니다. LEAK 시험(3~6단계)은 수행하지 않습니다.
> **용도**: 장비 설치 후 통신 상태 확인 및 디버깅 시 사용합니다. 실제 누설 시험(압력 계측)은 수행하지 않습니다.
## 7.7 SPEC 교차 검증 및 판정 저장 기준
## 7.8 측정 판정 및 저장 기준
시험 과정에서 프로그램(UI) 판정과 C28 센서 자체의 판정이 불일치하는 경우, 데이터베이스 및 로그 저장의 기준은 다음과 같습니다.
프로그램의 판정 스펙(상하한 규격) 기준과 누설 센서 자체의 내부 스펙 판정 기준이 일치하지 않는 경우, 저장소에는 다음과 같이 저장됩니다.
### 저장 기준 요약
* **최종 저장 판정 (Result)**: C28 센서 자체의 판정값과 무관하게, 항상 **프로그램(UI) 판정 기준(`programJudgment`)**으로 데이터베이스 및 로그에 저장됩니다.
* **동작 예시**: UI 상으로 합격(`OK`)이지만 C28 센서 자체적으로는 불합격(`NG`/`R`)인 경우, 최종 DB(`Result` 컬럼) 및 CSV 로그에는 **`OK` (합격)**로 저장됩니다.
* **최종 저장 판정 (Result)**: 센서 자체의 판정값과 무관하게, 항상 **프로그램 화면 판정 기준**으로 데이터베이스 및 로그에 저장됩니다.
* **동작 예시**: 프로그램 판정이 합격(`OK`)이지만 센서 자체의 내부 판정이 불합격(`NG` 등)인 경우에도, 최종 기록 및 CSV에는 **`OK` (합격)**로 기록됩니다.
### 교차 검증 실패 시의 동작 (Spec Mismatch)
* 프로그램 판정과 센서 자체 판정이 다르면 `specMismatch` 플래그가 `true`로 설정됩니다.
* 불일치 발생 시 콘솔 로그에 경고(`[TestProcess] {side} SPEC Mismatch!`)를 출력하고 UI 완료 이벤트 인자로 전달되지만, **DB에 저장되는 최종 판정 결과(`Result`)에는 영향을 미치지 않고 프로그램 판정값을 유지**합니다.
### 판정 스펙 불일치 시의 동작
* 양측의 판정 스펙이 다르면 불일치 로그가 기록되며 화면에 경고 팝업이 출력되지만, **최종 저장되는 판정 결과에는 영향을 미치지 않고 프로그램 판정값을 유지**합니다.
---

79
leak_test_project/Manual/08_로그_관리.md

@ -49,8 +49,6 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
> **참고**: 타임아웃 발생 시에도 측정값 `0.0000`, 판정 `NG`로 로그에 기록됩니다.
> **참고**: 프로그램은 구형 8개 컬럼 포맷(`Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment`)의 로그 파일에 대해서도 컬럼 크기를 기준으로 자동 감지 및 파싱(하위 호환성)을 수행합니다.
> **참고**: CSV 값에 쉼표, 따옴표, 줄바꿈이 포함된 경우 자동으로 이스케이프 처리됩니다.
---
@ -85,7 +83,7 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
| 항목 | 내용 |
|------|------|
| **위치** | 프로그램 실행 폴더 |
| **위치** | 프로그램 실행 폴더 (`C:\LeakTest\config.xml`) |
| **생성** | 최초 실행 시 기본값으로 자동 생성 |
| **수정** | Comm. Settings 또는 Parameters에서 저장 시 갱신 |
@ -98,6 +96,7 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
<Board4251Timeout>5000</Board4251Timeout>
<SpecUL>1.00</SpecUL>
<SpecLL>-1.00</SpecLL>
<EnableSpecMismatchAlarm>true</EnableSpecMismatchAlarm>
</AppConfig>
```
@ -110,6 +109,7 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
| `Board4251Timeout` | 5000 | (코드 내 고정) | 4251 보드 응답 대기 시간(ms) |
| `SpecUL` | 1.00 | Parameters | 누설 상한 규격값 |
| `SpecLL` | -1.00 | Parameters | 누설 하한 규격값 |
| `EnableSpecMismatchAlarm` | true | Comm. Settings | 스펙 불일치 알림 활성화 여부 (true/false) |
### Database.ini
@ -120,28 +120,43 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
| **수정** | 메모장 등의 텍스트 에디터를 통해 연결 주소 및 프로시저 정보 직접 수정 |
```ini
; =====================================================================
; Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)
; =====================================================================
; [데이터베이스 접속 상세 설정]
Db_Server=192.168.0.20
Db_Name=NE1aW_PT_SENSOR
Db_User=sa
Db_Password=sa1234
; Db_Server : 중앙 MSSQL 데이터베이스 서버의 IP 주소 또는 호스트명
; Db_Name : 검사 데이터를 저장할 대상 데이터베이스 이름
; Db_User : 데이터베이스 접속 계정 ID (예: sa)
; Db_Password: 데이터베이스 접속 계정의 비밀번호
Db_Server=127.0.0.1, 1433
Db_Name=Test_DB
Db_User=db_user
Db_Password=db_password
[Login]
; Procedure: 로그인 검증에 사용할 DB 저장 프로시저명입니다. (공란일 경우 기본 테이블 조회 쿼리 작동)
Procedure=dbo.CheckOperator
; ProcessName: 로그인 검증 시 전달할 공정 이름입니다.
ProcessName=EOL
; OfflinePreview: 오프라인 모드 활성화 여부 (true = DB 연결 없이 로그인 통과, false = 실제 DB 인증)
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 | 로그인 계정 검증용 저장 프로시저명 |
| `Db_Server` | `[서버IP], [포트번호]` (예: 127.0.0.1, 1433) | 데이터베이스 서버 IP 주소 및 포트 번호 |
| `Db_Name` | `[데이터베이스명]` (예: Test_DB) | 대상 데이터베이스(DB) 이름 |
| `Db_User` | `[사용자ID]` (예: db_user) | 데이터베이스 접속 계정 ID |
| `Db_Password` | `[비밀번호]` (예: db_password) | 데이터베이스 접속 계정 비밀번호 |
| `Procedure` | dbo.CheckOperator | 로그인 계정 검증용 데이터베이스 프로그램명 |
| `ProcessName` | EOL | 검증 공정명 |
| `OfflinePreview` | false | true 시 DB 연결 없이 로그인 즉시 성공 (오프라인 모드) |
> **SSL 인증 오류 방지**: DB 연결 시 SSL 인증서 신뢰 오류가 발생하는 환경을 방지하기 위해 프로그램 내부에서 자동으로 `TrustServerCertificate=True;` 옵션을 연결 문자열 끝에 추가하여 안정적인 동작을 보장합니다.
### LoginOptions.json / LoginHistory.json
| 항목 | 내용 |
@ -203,29 +218,29 @@ BoardNumber=0
매 시험 완료 시 검사 결과 및 로그인 정보가 데이터베이스에 실시간으로 저장(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`) | |
| 데이터베이스 항목명 | 구분 | 설명 |
| :--- | :--- | :--- |
| **`IC_SN`** | 문자형 (최대 10자) | 제품 고유 IC 일련번호 |
| **`PCB_Barcode`** | 문자형 (최대 10자) | 조회된 PCB 바코드 정보 |
| `Maker` | 문자형 (최대 10자) | 로그인 시 입력한 제조사명 |
| `Model` | 문자형 (최대 10자) | 로그인 시 입력한 검사 모델명 |
| `Variant_1` | 문자형 (최대 10자) | 로그인 시 입력한 파생 모델명 1 |
| `Variant_2` | 문자형 (최대 10자) | 로그인 시 입력한 파생 모델명 2 |
| `Operator` | 문자형 (최대 10자) | 검사자(작업자) ID |
| `Production_Date` | 날짜/시간형 | 검사가 완료된 시간 |
| `Line` | 문자형 (최대 30자) | 로그인 시 입력한 작업 라인명 |
| `Lot_No` | 문자형 (최대 8자) | 로그인 시 입력한 Lot 번호 |
| `Jig_No` | 문자형 (최대 10자) | 로그인 시 입력한 지그 번호 |
| `Channel` | 문자형 (최대 5자) | 검사가 수행된 채널 (LEFT / RIGHT) |
| `Spec_UL` | 숫자형 | 리크 판정 상한 기준값 |
| `Spec_LL` | 숫자형 | 리크 판정 하한 기준값 |
| `Leak_Value` | 숫자형 | 측정된 누설량 |
| `Result` | 문자형 (최대 5자) | 최종 판정 결과 (OK / NG) |
---

20
leak_test_project/Manual/09_오류_대응.md

@ -22,18 +22,19 @@
| 오류 메시지 | 원인 | 조치 방법 |
|------------|------|----------|
| `Sentinel C28 통신 타임아웃` | 30초 내 C28 센서 응답 없음 | ① 센서 전원 ON 확인 ② 시리얼 케이블 연결 확인 ③ BaudRate 설정 확인 (기본: 9600) |
| `통신 실패 (4251 보드 CHx 상태 타임아웃)` | 4251 보드가 응답 시간(15초) 내에 상태 응답을 회신하지 않음 | ① 4251 보드 전원 공급 및 케이블 연결선 확인 ② 통신 노이즈 방지 조치 확인 |
| `통신 실패 (4251 보드 CHx 상태 응답 시간 초과)` | 4251 보드가 대기 시간(15초) 내에 응답하지 않음 | ① 4251 보드 전원 공급 및 케이블 연결선 확인 ② 통신 노이즈 방지 조치 확인 |
| `통신 실패 (4251 보드 CHx 상태 이상 또는 Fail)` | 4251 보드로부터 정상 응답을 받지 못했거나 오류 코드가 반환됨 | ① 하드웨어 오결선 여부 점검 ② 채널 센서 구동 전원 확인 |
| `통신 실패 (4251 보드 CHx ID 대기 타임아웃)` | 4251 보드를 통해 제품 ID를 15초 내에 판독하지 못함 | ① 센서가 지그(Jig)에 올바르게 안착되었는지 물리적 확인 ② 접촉 단자 이물질 청소 |
| `SPEC 불일치 - 프로그램: OK, 센서: R` | 프로그램 SPEC과 센서 자체 SPEC의 판정이 다름 | ① 프로그램의 SPEC UL/LL 확인 (Parameters) ② C28 센서의 SPEC 설정 확인 ③ 양쪽 설정값 동기화 |
| `SPEC 불일치 - 프로그램: OK, 센서: R` | 프로그램 SPEC과 센서 자체 SPEC의 판정이 다름 | ① 프로그램의 SPEC UL/LL 확인 (Parameters) ② C28 센서의 SPEC 설정 확인 ③ 양쪽 설정값 동기화 (※ 통신 설정에서 경고 알림 스위치 상태를 확인하세요) |
| `LEAK 센서 데이터 오류` | C28에서 수신된 데이터 파싱 실패 | ① 센서 통신 상태 확인 ② 케이블 접촉 불량 확인 ③ BaudRate 불일치 확인 |
| `DB 저장 실패` | 중앙 MSSQL DB 서버에 검사 데이터를 저장하지 못함 | ① PC 네트워크 케이블 및 통신 연결 확인 ② Database.ini 파일의 접속 정보(IP/비밀번호 등) 확인 ③ 로컬 일일 CSV 로그 파일(`C:\LeakTest\Logs\yyyy-MM-dd.csv`)에 백업이 잘 저장되었는지 확인 |
| `DB 저장 실패: [오류 원인] 상세 메시지` | 데이터베이스 서버에 검사 데이터를 저장하지 못함 | ① 네트워크 케이블 및 서버 통신 연결 확인 ② Database.ini 설정 파일의 접속 정보(IP/비밀번호 등) 확인 ③ 로컬 일일 CSV 로그 파일에 백업이 정상적으로 저장되었는지 확인 |
| `바코드 매핑 실패: [오류 원인] 상세 메시지` | 데이터베이스에서 해당 `IC_SN`에 매칭되는 바코드 정보를 찾지 못함 | ① 데이터베이스 서버 네트워크 연결 확인 ② Housing_Assembly 테이블에 대상 제품의 바코드 정보 매핑 상태 점검 |
---
## 9.3 SPEC 교차 검증 경고 팝업
프로그램 판정과 센서 판정이 불일치하면 팝업 경고가 표시됩니다:
프로그램 판정과 센서 판정이 불일치할 때, 통신 설정 화면의 **`스펙 불일치 알림 활성화`** 옵션이 켜져(ON) 있는 경우 팝업 경고가 표시됩니다:
```
[!] SPEC 교차 검증 경고
@ -41,7 +42,8 @@
[확인]
```
이 경고는 검사 자체를 중단하지는 않지만, **프로그램의 판정 결과**가 최종 기록에 사용됩니다.
- 만약 이 옵션을 **해제(OFF)**한 경우, 이 팝업 경고 대화상자는 나타나지 않으며, 메인 화면 하단의 **오류 메시지** 텍스트 박스에만 불일치 로그가 기록됩니다.
- 이 경고는 검사 자체를 중단하지는 않으며, **프로그램의 판정 결과**가 최종 DB 및 로컬 로그 기록에 사용됩니다.
---
@ -52,7 +54,7 @@
| PCI-7432 보드 미인식 | **시뮬레이션 모드**로 자동 전환 | DIO 입출력이 동작하지 않음 (실제 입출력 포트 제어 불가) |
| 보드 런타임 오류 | 좌/우 채널 오류 메시지에 `[DIO Board Error]` 표시 | 오류 내용에 따라 조치 |
> **시뮬레이션 모드**: ADLINK 드라이버가 신설되지 않은 개발 환경에서도 프로그램의 나머지 기능(통신 설정, 데이터 조회 등)을 테스트할 수 있도록 자동 전환됩니다.
> **시뮬레이션 모드**: ADLINK 드라이버가 설치되지 않은 환경에서도 프로그램의 기능(통신 설정, 데이터 조회 등)을 사전에 테스트할 수 있도록 제공되는 가상 동작 모드입니다.
---
@ -90,9 +92,9 @@
| 순서 | 처리 내용 |
|------|----------|
| 1 | 시험 프로세스 스레드 종료 (좌/우 모두) |
| 2 | 대기 중인 세마포어 해제 |
| 3 | DIO 이벤트 구독 해제 |
| 4 | C28 센서 이벤트 구독 해제 |
| 2 | 대기 중인 순서 잠금 해제 |
| 3 | 디지털 입출력(DIO) 신호 감지 중단 |
| 4 | 누설 센서 신호 감지 중단 |
| 5 | Sentinel C28 시리얼 통신 연결 해제 |
| 6 | 4251 보드 시리얼 통신 연결 해제 |
| 7 | DIO `PC_ON` 출력 **OFF** |

550
leak_test_project/Manual/Manual_Merged.html

File diff suppressed because it is too large

438
leak_test_project/Manual/Manual_Merged.md

@ -1,4 +1,4 @@
<style>
<style>
/*
* A4 용지 기준 PDF 출력을 위한 스타일시트 (pdf_style.css)
*/
@ -217,7 +217,7 @@ a:hover {
# Pressure Leak Inspect System - 사용자 메뉴얼
> **버전**: 1.0.0 | **최종 수정**: 2026-06-25
> **버전**: 1.0.0 | **최종 수정**: 2026-07-01
---
@ -244,7 +244,7 @@ a:hover {
처음 사용하는 경우 아래 순서를 따르세요:
1. **하드웨어 연결 확인** → [시스템 개요 및 요구사항](#1-시스템-개요-및-요구사항) 참조
2. **프로그램 실행 및 로그인**`leak_test_project.exe` 실행 후 로그인 완료 (오프라인 테스트는 `Database.ini`에서 `OfflinePreview=true` 설정)
2. **프로그램 실행 및 로그인**바탕화면의 **바로가기 아이콘**을 실행하여 로그인 완료 (오프라인 테스트는 `Database.ini`에서 `OfflinePreview=true` 설정)
3. **통신 설정** → 하단 메뉴 `Comm. Settings` → COM 포트 선택 → `설정 저장`
4. **시험 설정** → 하단 메뉴 `Parameters` → SPEC UL/LL 입력 → `설정 저장`
5. **시험 시작** → DIO 시작 신호 또는 `ID Test (L/R)` 버튼으로 동작 확인
@ -263,7 +263,7 @@ a:hover {
## 1.1 시스템 개요
**Pressure Leak Inspect System**은 제품의 압력 누설(Leak)을 자동으로 검사하는 WPF 기반 데스크탑 애플리케이션입니다. 생산 라인에서 DIO 시작 신호를 받아 제품 ID를 판독하고, 누설 시험을 수행한 뒤 합격/불합격을 자동 판정합니다.
**Pressure Leak Inspect System**은 제품의 압력 누설(Leak)을 자동으로 검사하는 데스크탑 프로그램입니다. 생산 라인에서 DIO 시작 신호를 받아 제품 ID를 판독하고, 누설 시험을 수행한 뒤 합격/불합격을 자동 판정합니다.
### 주요 기능 요약
@ -276,7 +276,7 @@ a:hover {
| **PCI-7432 DIO 보드** | 디지털 입출력 제어 (시작 신호 수신, OK/NG 출력) |
| **검사 기록 관리** | 날짜/판정/ID별 필터링 검색 및 CSV 파일 내보내기를 지원합니다 |
| **SPEC 교차 검증** | 프로그램 판정과 센서 자체 판정을 비교하여 불일치 시 경고합니다 |
| **순차 실행 보장** | 세마포어 잠금으로 좌/우 채널 동시 시험을 방지합니다 |
| **순차 실행 보장** | 중복 실행 방지 기능으로 좌/우 채널의 동시 시험을 방지합니다 |
---
@ -285,7 +285,7 @@ a:hover {
| 항목 | 요구사항 | 비고 |
|------|----------|------|
| **운영체제** | Windows 10 이상 (64bit 권장) | 32bit도 지원 가능 |
| **.NET Framework** | 4.7.2 이상 | Windows 10에 기본 포함 |
| **.NET 런타임** | .NET 9 | 별도 설치 필요 없음 (자체 포함 배포) |
| **드라이버** | ADLINK PCIS-DASK 드라이버 | `PCI-Dask.dll` 필요 |
| **시리얼 포트** | 최소 2개의 COM 포트 | C28 센서용 + 4251 보드용 |
| **화면 해상도** | 1024×768 이상 | 최대화 실행 시 1920×1080 권장 |
@ -427,15 +427,19 @@ a:hover {
## 2.2 프로그램 실행
### 실행 방법
1. `leak_test_project.exe` 파일을 **더블 클릭**하여 실행합니다.
1. 바탕화면의 **바로가기 아이콘**을 더블 클릭하여 실행합니다.
2. 프로그램이 전체화면(Maximized) 모드로 시작됩니다.
### 최초 실행 시 자동 생성되는 파일
| 파일 | 위치 | 설명 |
|------|------|------|
| `config.xml` | 프로그램 실행 폴더 | 통신 포트, BaudRate, SPEC 값 등 모든 설정을 저장 |
| `Settings/DioConfig.ini` | Settings 하위 폴더 | DIO 보드 타입, 입출력 포인트 정의 |
| 파일 / 폴더 | 위치 | 설명 |
|-------------|------|------|
| `config.xml` | 프로그램 실행 폴더 (`C:\LeakTest\`) | 통신 포트, BaudRate, SPEC 값 등 모든 설정을 저장 |
| `Settings\DioConfig.ini` | `C:\LeakTest\Settings\` | DIO 보드 타입, 입출력 포인트 정의 |
| `Database.ini` | `C:\LeakTest\` | DB 접속 정보 및 로그인 인증 방식 설정 (최초 실행 시 기본 템플릿으로 생성) |
| `LoginOptions.json` | `C:\LeakTest\` | 로그인 화면 자동완성 드롭다운 선택지 저장 |
| `LoginHistory.json` | `C:\LeakTest\` | 최근 로그인 정보 최대 10건 자동 저장 |
| `Logs\` 폴더 | `C:\LeakTest\Logs\` | 날짜별 CSV 검사 로그 및 시스템 로그 저장 위치 |
### 프로그램 시작 시 자동 수행 동작
@ -509,14 +513,14 @@ a:hover {
### 로그인 방식 설정 (Database.ini)
윈도우 공용 폴더(`C:\ProgramData\LeakTest\Database.ini`)에 위치한 `Database.ini` 파일 설정에 따라 로그인 인증 방식이 달라집니다. (해당 파일이 누락된 경우 프로그램 최초 실행 시 기본 템플릿 정보와 함께 경로상에 자동으로 생성됩니다. ClickOnce 배포 방식을 사용하는 경우에도 프로그램 업데이트 시 설정 데이터가 유실되지 않고 영구 보존됩니다.)
프로그램 배포 폴더(`C:\LeakTest\Database.ini`)에 위치한 `Database.ini` 파일 설정에 따라 로그인 인증 방식이 달라집니다. 해당 파일이 누락된 경우 프로그램 최초 실행 시 기본 템플릿 정보와 함께 해당 경로에 자동으로 생성됩니다. 모든 설정 파일이 배포 폴더에 함께 위치하므로 경로 관리가 단순합니다.
```ini
; [데이터베이스 접속 상세 설정]
Db_Server=192.168.0.20
Db_Name=NE1aW_PT_SENSOR
Db_User=sa
Db_Password=sa1234
Db_Server=[데이터베이스_서버_IP],[포트번호]
Db_Name=[데이터베이스_이름]
Db_User=[접속_계정_ID]
Db_Password=[접속_비밀번호]
[Login]
Procedure=dbo.CheckOperator
@ -524,7 +528,7 @@ ProcessName=EOL
OfflinePreview=false
```
- **온라인 인증 모드 (`OfflinePreview=false`)**: 지정된 개별 데이터베이스 접속 정보(`Db_Server`, `Db_Name`, `Db_User`, `Db_Password`)를 기반으로 데이터베이스에 접속하여 Stored Procedure(`Procedure`)로 ID/PW 존재 여부를 직접 확인합니다. DB 서버 연결 실패나 미등록 사용자일 경우 로그인이 거부됩니다.
- **온라인 인증 모드 (`OfflinePreview=false`)**: 지정된 데이터베이스 접속 정보(`Db_Server`, `Db_Name`, `Db_User`, `Db_Password`)를 기반으로 데이터베이스에 접속하여 사용자 계정의 유효성을 검증합니다. DB 서버 연결 실패나 미등록 사용자일 경우 로그인이 거부됩니다.
- **오프라인 테스트 모드 (`OfflinePreview=true`)**: 데이터베이스를 거치지 않고, 필수 입력 값을 채우면 즉시 로그인이 허용됩니다. 네트워크 연결이 안 된 시뮬레이션 및 테스트 환경에서 사용합니다.
@ -658,20 +662,22 @@ OfflinePreview=false
- **LEFT LEAK TESTER** 또는 **RIGHT LEAK TESTER**
- 짙은 남색 배경에 백색 볼드 텍스트로 채널 구분을 명시합니다.
#### ② 시작 시간 (Row 1)
#### ② 시작/종료 시간 (Row 1)
| 항목 | 설명 |
|------|------|
| 라벨 | `시작 시간` (회색 헤더 배경) |
| 값 | `yyyy-MM-dd HH:mm:ss` 형식 (예: `2026-05-08 10:30:15`) |
| 갱신 시점 | 시험이 시작될 때마다 현재 시각으로 자동 갱신 |
| 라벨 | `시작/종료` (회색 헤더 배경) |
| 값 | `시작시간 / 종료시간` 형식 (예: `2026-05-08 10:30:15 / -` 또는 `2026-05-08 10:30:15 / 10:30:45`) |
| 갱신 시점 | 시험이 시작될 때 `시작시간 / -` 형태로 갱신되며, 시험 완료(DB 저장) 시점에 종료 시간이 함께 기록됩니다. |
#### ③ ID (Row 2)
#### ③ IC_SN / PCB_Barcode (Row 2~3) - **식별자 2행 분리 표시**
| 항목 | 설명 |
|------|------|
| 라벨 | `ID` (회색 헤더 배경) |
| 값 | 16자리 제품 ID 표시 |
| 데이터 소스 | 4251 보드를 통해 제품 ID 센서에서 읽은 값 |
| 갱신 시점 | 시험 시작 시 초기화, 센서 판독 완료 시 표시 |
| 라벨 | `IC_SN` / `PCB_Barcode` (각각 회색 헤더 배경, 개별 행으로 표시) |
| 값 | 각각 제품의 IC 일련번호 및 이에 매핑되는 PCB 바코드 고유 식별 정보 |
| 데이터 소스 | **IC_SN**: 4251 보드로 제품 ID 센서에서 판독한 값<br>**PCB_Barcode**: 판독한 `IC_SN`을 기준으로 데이터베이스에서 조회한 바코드 값 (오프라인 모드 시 임시 데이터 주입) |
| 갱신 시점 | 시험 시작 시 빈 값(`-`)으로 초기화, 판독 및 조회 완료 시 표시 |
> **참고**: 텍스트 잘림 방지 및 가독성 향상을 위해 IC_SN과 PCB_Barcode를 별도의 행으로 나누어 표시합니다. 프로그램 창 크기를 소형으로 조절하더라도 텍스트가 잘리지 않고 정상 표시됩니다.
#### ④ 측정값 + SPEC (Row 3) - **가장 큰 영역**
좌측에 측정값, 우측에 SPEC 규격이 함께 표시됩니다.
@ -798,16 +804,27 @@ OfflinePreview=false
| **Sensor BaudRate** | 드롭다운 | 9600 / 19200 / 38400 / 115200 | `9600` | C28 센서 통신 속도. 센서 설정과 일치해야 함 |
| **Sensor Port** | 드롭다운 | 시스템 감지 COM 포트 목록 | `COM1` | C28 센서가 연결된 시리얼 포트 |
## 4.4 버튼 설명
## 4.4 스펙 불일치 알림 설정 (화면 하단 영역)
| 항목 | UI 타입 | 선택 가능한 값 | 기본값 | 설명 |
|------|---------|---------------|--------|------|
| **스펙 불일치 알림 활성화** | 체크박스 | 체크(ON) / 체크 해제(OFF) | `체크(ON)` | 프로그램 판정과 센서 자체 판정이 맞지 않을 때의 알림 팝업 창 발생 여부 설정 |
> **동작 설명**:
> - **ON (기본값)**: 판정이 불일치할 때 메인 화면의 오류 메시지 창에 로그가 기재됨과 동시에, 경고 대화상자("프로그램 스펙과 센서의 스펙이 서로 맞지 않습니다.") 팝업이 출력되어 작업자 인지를 강제합니다.
> - **OFF**: 판정이 불일치하더라도 경고 팝업을 띄우지 않고, 화면의 오류 메시지 영역에만 텍스트로 기록을 남깁니다. (콘솔 로그 기록 등은 유지)
> - 이 설정은 `config.xml` 내의 `EnableSpecMismatchAlarm` 옵션에 영속 저장되어 프로그램 재작동 시에도 마지막 상태가 자동 유지됩니다.
## 4.5 버튼 설명
| 버튼 | 특징 | 동작 |
|------|------|------|
| **설정 저장** | 녹색 계열 | config.xml에 저장 → 기존 연결 해제 → 새 설정으로 재연결 → "통신 설정이 저장되었습니다." 메시지 → 창 닫힘 |
| **닫기** | 회색 계열 | 변경 사항 저장하지 않고 창 닫기 |
## 4.5 저장 시 내부 동작 순서
## 4.6 저장 시 내부 동작 순서
1. 현재 선택된 값을 `config.xml`에 저장
1. 현재 선택된 값을 `config.xml`에 저장 (스펙 불일치 알림 활성화 여부 포함)
2. 기존 Sentinel C28 시리얼 연결 해제
3. 기존 4251 보드 시리얼 연결 해제
4. 시험 프로세스 스레드 종료
@ -889,68 +906,68 @@ OfflinePreview=false
| 항목 | UI 타입 | 기본값 | 설명 |
|------|---------|--------|------|
| **조회 기간** | DatePicker (달력) x 2 | 오늘로부터 7일 전 ~ 오늘 | 조회 대상 날짜 범위 설정 (달력 확대 표시 적용) |
| **판정 결과** | 드롭다운 ComboBox | `[전체]` / `OK` / `NG` | 판정 결과에 따른 필터링 (기본값: 전체) |
| **조회 기간** | 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`) 컬럼에 대한 부분 일치 검색 | (빈 값 = 필터 제외) |
| **ID 검색** | TextBox (텍스트 입력) | `IC_SN` 또는 `PCB_Barcode` 열에 대한 부분 일치 (LIKE) 검색을 수행합니다. | (빈 값 = 필터 제외) |
| **작업자** | TextBox (텍스트 입력) | 작업자 ID (`Operator`) 열에 대한 부분 일치 검색을 수행합니다. | (빈 값 = 필터 제외) |
| **Lot No** | TextBox (텍스트 입력) | 로트 번호 (`Lot_No`) 열에 대한 부분 일치 검색을 수행합니다. | (빈 값 = 필터 제외) |
| **모델** | TextBox (텍스트 입력) | 모델명 (`Model`) 열에 대한 부분 일치 검색을 수행합니다. | (빈 값 = 필터 제외) |
## 6.3 조회 및 취소 버튼
| 버튼 | 색상 | 동작 |
|------|------|------|
| **Search** | 파란색 계열 | 설정된 2행의 모든 필터 조건으로 로컬 로그 CSV 백업 파일을 부분일치 검색하여 그리드에 표시합니다. (안정성을 위해 DB 접속 조회가 아닌 로컬 파일 조회로 작동합니다.) |
| **Cancel (취소)** | 빨간색 계열 | 조회 중일 때 로딩 화면 내부에 나타나며, 진행 중인 파일 검색 작업을 즉시 중단하고 화면을 복귀시킵니다. |
| **Search** | 파란색 계열 | 설정된 필터 조건으로 로컬 로그 CSV 백업 파일을 검색하여 그리드에 표시합니다. (시스템 안정성을 위해 데이터베이스 직접 조회 대신 로컬 백업 파일을 조회하도록 설계되었습니다.) |
| **Cancel (취소)** | 빨간색 계열 | 조회 중일 때 로딩 화면 내부에 나타나며, 진행 중인 파일 검색 작업을 중단하고 이전 화면으로 복귀합니다. |
### Search 버튼 동작 상세
1. 설정된 조회 기간 날짜를 직접 일자별로 조합하여 해당하는 일일 로그 CSV 파일만 콕 집어 정밀 탐색합니다. (전체 디렉토리 풀 스캔 차단으로 조회 속도가 매우 빠릅니다.)
2. 검색 조건(판정, ID, 작업자, Lot No, 모델)에 맞는 대상을 인메모리 부분일치 검색 처리합니다. (과거 구형 8개 컬럼 포맷 파일도 하위 호환되어 정상 파싱됩니다.)
3. 검색 결과가 없으면 "해당 조건의 데이터가 없습니다." 알림 표시.
1. 지정된 조회 기간에 해당하는 일별 로그 CSV 파일만 조회하여 검색을 수행합니다. (불필요한 파일 스캔을 제한하여 검색 속도를 향상시켰습니다.)
2. 검색 조건(판정, ID, 작업자, Lot No, 모델)에 맞는 데이터를 인메모리 부분 일치 방식으로 검색합니다.
3. 검색 결과가 존재하지 않을 경우 "해당 조건의 데이터가 없습니다." 알림 표시합니다.
### 조회 중 로딩 및 Cancel 버튼 동작 상세
1. Search 버튼 클릭 시 화면 전체에 "Searching Logs..." 라는 반투명 어두운 로딩 오버레이가 활성화되어 작업 중임을 시각적으로 나타냅니다.
2. 데이터가 많아 조회가 오래 걸릴 경우 로딩 오버레이의 **Cancel (취소)** 버튼을 누르면 즉시 조회가 중단되고, "조회가 취소되었습니다." 알림과 함께 메인 화면으로 안전하게 복귀합니다.
## 6.4 데이터 그리드 컬럼 (DB EOL 스키마 표준 규격)
| 컬럼 헤더명 | 바인딩 프로퍼티 | 정렬 | 설명 |
|------|------|------|------|
| **IC_SN** | `IcSn` | 좌측 | 제품 고유 IC SN 시리얼 번호 (PK/FK) |
| **PCB_Barcode** | `PcbBarcode` | 좌측 | PCB 바코드 고유 식별 정보 (UK) |
| **Maker** | `Maker` | 좌측 | 로그인 시 입력된 제조사명 |
| **Model** | `Model` | 좌측 | 로그인 시 입력된 검사 모델명 |
| **Variant_1** | `Variant1` | 좌측 | 로그인 시 입력된 세부 파생 모델 1 |
| **Variant_2** | `Variant2` | 좌측 | 로그인 시 입력된 세부 파생 모델 2 |
| **Operator** | `Operator` | 좌측 | 작업 진행자 사용자 ID |
| **Production_Date** | `ProductionDate` | 좌측 | 검사 수행 및 완료 일시 (yyyy-MM-dd HH:mm:ss) |
| **Line** | `Line` | 좌측 | 로그인 공정 작업 라인 |
| **Lot_No** | `LotNo` | 좌측 | 생산 Lot 번호 |
| **Jig_No** | `JigNo` | 좌측 | 사용 지그(Jig) 번호 |
| **Channel** | `Channel` | 좌측 | 검사 채널 (`LEFT` 또는 `RIGHT`) |
| **Spec_UL** | `SpecUL` | 좌측 | 누설 상한 규격값 (sccm) |
| **Spec_LL** | `SpecLL` | 좌측 | 누설 하한 규격값 (sccm) |
| **Leak_Value** | `MeasuredValue` | **우측** | 측정된 누설 리크량 (**볼드 표시**) |
| **Result** | `Judgment` | **중앙** | 최종 판정 결과 (`OK` 초록 / `NG` 빨강 볼드 강조) |
### 그리드 스타일
- 격자선: 수평/수직 연회색 격자선 표시
- 홀수 행 배경: 흰색, 짝수 행 배경: 연회색 배경으로 행 식별 편의 제공
- 선택된 행: 연파랑 배경에 파란색 텍스트로 강조
- 컬럼 헤더 클릭 시 해당 열 기준 오름차순/내림차순 정렬 가능 (컬럼 순서 변경 불가)
- 최소 50행이 항상 표시됩니다 (데이터가 적어도 격자 형태 유지를 위해 빈 행으로 채워짐)
1. Search 버튼 클릭 시 화면 전체에 "Searching Logs..." 라는 반투명 오버레이가 활성화되어 검색이 진행 중임을 나타냅니다.
2. 검색이 지연될 경우 로딩 오버레이의 **Cancel (취소)** 버튼을 누르면 즉시 조회가 중단되고, "조회가 취소되었습니다." 메시지 박스 팝업 후 자동으로 메인 화면으로 복귀합니다.
## 6.4 데이터 조회 결과 표(그리드) 항목 안내
| 컬럼 헤더명 | 정렬 | 설명 |
|------|------|------|
| **IC_SN** | 좌측 | 제품 고유 IC 시리얼 번호 |
| **PCB_Barcode** | 좌측 | PCB 바코드 고유 식별 정보 |
| **Maker** | 좌측 | 로그인 시 입력된 제조사명 |
| **Model** | 좌측 | 로그인 시 입력된 검사 모델명 |
| **Variant_1** | 좌측 | 로그인 시 입력된 세부 파생 모델 1 |
| **Variant_2** | 좌측 | 로그인 시 입력된 세부 파생 모델 2 |
| **Operator** | 좌측 | 작업 진행자 사용자 ID |
| **Production_Date** | 좌측 | 검사 수행 및 완료 일시 (yyyy-MM-dd HH:mm:ss) |
| **Line** | 좌측 | 로그인 공정 작업 라인 |
| **Lot_No** | 좌측 | 생산 Lot 번호 |
| **Jig_No** | 좌측 | 사용 지그(Jig) 번호 |
| **Channel** | 좌측 | 검사 채널 (`LEFT` 또는 `RIGHT`) |
| **Spec_UL** | 좌측 | 누설 상한 규격값 (sccm) |
| **Spec_LL** | 좌측 | 누설 하한 규격값 (sccm) |
| **Leak_Value** | **우측** | 측정된 누설 리크량 (**볼드 표시**) |
| **Result** | **중앙** | 최종 판정 결과 (`OK` 초록 / `NG` 빨강 볼드 강조) |
### 표(그리드) 스타일
- 격자선: 수평 및 수직 연회색 격자선을 표시합니다.
- 교대 행 배경: 홀수 행은 흰색, 짝수 행은 연회색 배경을 적용하여 가독성을 높였습니다.
- 선택된 행: 연파란색 배경에 파란색 텍스트로 강조합니다.
- 컬럼 헤더 정렬: 헤더 클릭 시 해당 열 기준으로 오름차순 또는 내림차순 정렬이 가능합니다. (컬럼 순서 변경은 제한됩니다.)
- 격자 유지: 조회된 데이터 개수가 50개 미만이더라도 화면 구성을 유지하기 위해 나머지 행은 빈 칸으로 채워져 표시됩니다.
## 6.5 화면 닫기
| 방법 | 설명 |
|------|------|
| 하단 **Close** 버튼 클릭 | 메인 화면으로 복귀 |
| 하단 메뉴 **Data** 버튼 재클릭 | 토글 동작으로 메인 화면으로 복귀 |
| 하단 **Close** 버튼 클릭 | 메인 화면으로 복귀합니다. |
| 하단 메뉴 **Data** 버튼 재클릭 | 토글 동작을 통해 메인 화면으로 복귀합니다. |
<div class="page-break"></div>
@ -961,7 +978,7 @@ OfflinePreview=false
## 7.1 개요
자동 시험은 DIO 시작 신호(LEFT_START 또는 RIGHT_START)에 의해 트리거되며, 7단계로 진행됩니다. 좌/우 채널은 각각 독립적인 백그라운드 스레드에서 실행되지만, **세마포어 잠금**을 통해 동시 실행은 방지됩니다.
자동 시험은 시작 신호(LEFT_START 또는 RIGHT_START)에 의해 작동하며, 7단계로 진행됩니다. 좌/우 채널은 각각 독립적인 백그라운드 스레드에서 실행되지만, **중복 실행 방지 기능**을 통해 동시 시험은 방지됩니다.
## 7.2 전체 프로세스 흐름도
@ -969,16 +986,16 @@ OfflinePreview=false
<!-- Trigger -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<div style="font-weight: bold; color: #2f3542;">DIO 시작 신호 감지 <span style="font-weight: normal; font-size: 8pt; color: #747d8c;">(LEFT_START 또는 RIGHT_START: OFF➔ON)</span></div>
<div style="font-weight: bold; color: #2f3542;">시작 신호 감지 <span style="font-weight: normal; font-size: 8pt; color: #747d8c;">(좌측/우측 시작 신호 입력 시)</span></div>
</div>
<!-- Semaphore -->
<div style="margin-bottom: 20px; position: relative; background-color: #f1f2f6; padding: 10px; border-radius: 4px; border-left: 3px solid #f6b93b;">
<span style="position: absolute; left: -26px; top: 14px; width: 10px; height: 10px; background-color: #f6b93b; border-radius: 50%;"></span>
<strong>세마포어 잠금 대기</strong>
<strong>중복 실행 방지 대기</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
잠금 불가 시: "대기중" 메시지 표시 후 대기<br>
잠금 획득 시: 다음 단계로 진행 (좌/우 채널 동시 시험 방지)
다른 채널이 시험 중인 경우: "대기중" 메시지를 표시하며 순서 대기<br>
시험 가능 상태인 경우: 다음 단계 진행 (좌/우 채널 동시 시험 방지)
</div>
</div>
@ -987,59 +1004,69 @@ OfflinePreview=false
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">1단계: 시험 시작 (초기화)</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
OK/NG DIO 출력 초기화 (OFF)<br>
합격/불합격 신호 출력 초기화 (OFF)<br>
• 메인 화면 결과 표시 초기화 및 시작 시간 갱신<br>
Sentinel C28 LEAK 센서 수신 이벤트 버퍼 초기화
누설 센서 수신 데이터 초기화
</div>
</div>
<!-- Step 2 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">2단계: 센서 정보 읽기 (ID 판독)</strong>
<strong style="color: #1e3799;">2단계: 제품 ID 판독</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 4251 보드를 통해 제품 ID 센서에서 16자리 ID 판독<br>
<span style="color: #ff4757; font-weight: bold;">[실패 시]</span> "ID 읽기 실패" 오류 출력 후 NG 판정 조기 종료 (DB 비동기 저장)
판독 장치(4251 보드)를 통해 제품 ID 센서에서 16자리 ID 판독<br>
<span style="color: #ff4757; font-weight: bold;">[실패 시]</span> "ID 읽기 실패" 오류 출력 후 시험이 종료되며 결과는 기록 및 저장에서 제외됩니다.
</div>
</div>
<!-- Step 3 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">3단계: LEAK 시험 수행 (계측)</strong>
<div style="margin-bottom: 15px; position: relative; background-color: #f8f9fa; padding: 10px; border-radius: 4px; border-left: 3px solid #1e3799;">
<span style="position: absolute; left: -26px; top: 14px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">3단계: PCB 바코드 데이터베이스 조회</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 화면에 "LEAK 시험중" 표시<br>
• Sentinel C28 센서의 최종 압력 누설 측정 결과 대기 (최대 30초)<br>
<span style="color: #ff4757; font-weight: bold;">[타임아웃]</span> "Sentinel C28 통신 타임아웃" 오류 후 NG 판정 종료
• 데이터베이스에서 제품 ID(`IC_SN`)에 매칭되는 `PCB_Barcode` 조회 (5초 대기 시간 초과 제한)<br>
<span style="color: #ff4757; font-weight: bold;">[실패/누락 시]</span> "바코드 매핑 실패" 오류 출력 후 즉시 종료되며 결과는 기록 및 저장에서 제외됩니다.
</div>
</div>
<!-- Step 4 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">4단계: 합격 판정</strong>
<strong style="color: #1e3799;">4단계: 누설 시험 수행 (계측)</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
<span style="font-family: monospace;">SPEC LL ≤ 측정값 ≤ SPEC UL</span> 이면 **OK (합격)**, 범위를 벗어나면 **NG (불합격)**
• 화면에 "LEAK 시험중" 표시<br>
• 누설 센서(Sentinel C28)로부터 측정 결과 수신 대기 (최대 30초)<br>
<span style="color: #ff4757; font-weight: bold;">[시간 초과 시]</span> 통신 시간 초과 오류 발생 및 불합격(NG) 판정으로 자동 종료 (측정값 9.9999로 데이터베이스에 저장)
</div>
</div>
<!-- Step 5 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">5단계: SPEC 교차 검증</strong>
<strong style="color: #1e3799;">5단계: 합격 판정</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 프로그램(UI) 판정과 센서 자체 판정(OK/A/P vs NG/R/F)을 상호 비교<br>
• 불일치 시 `SpecMismatch` 플래그 설정 및 경고 팝업 표시
• 규격 하한값 ≤ 측정값 ≤ 규격 상한값 이면 **OK (합격)**, 범위를 벗어나면 **NG (불합격)**로 판정합니다.
</div>
</div>
<!-- Step 6 -->
<div style="margin-bottom: 15px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">6단계: 판정 스펙 교차 검증</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 프로그램 판정과 센서 자체 판정을 비교 분석합니다.<br>
• 판정이 서로 다를 경우 화면에 경고 창을 표시합니다. (통신 설정에서 알림 창 표시를 끌 수 있습니다.)
</div>
</div>
<!-- Step 7 -->
<div style="margin-bottom: 20px; position: relative;">
<span style="position: absolute; left: -26px; top: 4px; width: 10px; height: 10px; background-color: #1e3799; border-radius: 50%;"></span>
<strong style="color: #1e3799;">6단계: 결과 출력 및 저장</strong>
<strong style="color: #1e3799;">7단계: 결과 출력 및 저장</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 로컬 CSV 로그 파일 기록 및 MSSQL DB에 비동기 저장<br>
• 판정 결과에 따른 DIO 하드웨어 출력 (OK 또는 NG 포트 ON)<br>
• 로컬 CSV 백업 파일 기록 및 데이터베이스에 검사 결과 저장<br>
• 판정 결과에 따라 외부 설비에 출력 신호(OK 또는 NG 포트) 전송<br>
• 메인 화면에 최종 측정값 및 판정 출력 ("시험 완료" 표시)
</div>
</div>
@ -1047,71 +1074,80 @@ OfflinePreview=false
<!-- End -->
<div style="position: relative; background-color: #f1fff5; padding: 10px; border-radius: 4px; border-left: 3px solid #2ed573;">
<span style="position: absolute; left: -26px; top: 14px; width: 10px; height: 10px; background-color: #2ed573; border-radius: 50%;"></span>
<strong style="color: #2f855a;">세마포어 잠금 해제</strong>
<strong style="color: #2f855a;">순서 잠금 해제</strong>
<div style="font-size: 8.5pt; color: #57606f; margin-top: 3px;">
• 시험 종료 후 잠금 해제 → 대기 중인 채널이 있을 경우 350ms 안정화 대기 후 자동 시작
• 시험 종료 후 순서 대기가 해제되며, 대기 중이던 다른 채널이 0.35초 대기 후 자동으로 시험을 개시합니다.
</div>
</div>
</div>
## 7.3 순차 실행 (세마포어 잠금)
## 7.3 좌/우 채널 순차 실행 및 대기
좌/우 채널은 **동시에 시험을 수행할 수 없습니다**. 이는 하드웨어 통신 충돌을 방지하기 위함입니다.
좌/우 채널은 **동시에 시험을 수행할 수 없습니다**. 이는 하드웨어 통신 간섭 및 충돌을 방지하기 위함입니다.
| 상황 | 동작 |
|------|------|
| 한 채널만 시작 신호 | 즉시 시험 시작 |
| 양쪽 동시 시작 신호 | 먼저 잠금을 획득한 채널이 시험 진행, 다른 채널은 "대기중" 표시 |
| 시험 완료 후 | 대기 중인 채널이 자동으로 시험 시작 (350ms 안정화 대기 후) |
| 한 채널만 시험 시작 신호 발생 | 즉시 시험을 시작합니다. |
| 양쪽 채널 동시 신호 발생 | 먼저 순서를 획득한 채널이 시험을 진행하며, 다른 채널은 "대기중"을 표시하며 대기합니다. |
| 시험 완료 후 | 대기 중이던 채널이 자동으로 시험을 시작합니다. (안정화를 위해 0.35초의 대기 시간이 적용됩니다.) |
> **참고**: 하드웨어 통신 안정화를 위해 이전 채널의 시험 종료 후 **0.35초의 여유 시간**이 자동으로 적용됩니다.
## 7.4 시간 초과 (타임아웃) 처리
누설 센서에서 30초 이내에 계측 결과 응답이 없으면 다음과 같이 처리됩니다.
1. 화면의 오류 메시지 영역에 "통신 시간 초과" 오류 메시지를 표시합니다. (데이터베이스 저장 실패 시 "DB 저장 실패" 사유도 함께 노출)
2. 해당 채널의 불합격(NG) 물리 신호를 내보냅니다.
3. 측정값 `9.9999`, 판정 `NG`로 기록을 생성합니다.
4. 로컬 CSV 로그 파일 및 데이터베이스에 검사 결과를 기록합니다. (단, 이전 단계인 제품 ID 판독에 성공한 상태여야 저장이 이루어집니다.)
> **참고**: 하드웨어 통신 안정화를 위해, 이전 채널 시험 종료 후 **350ms의 여유 시간**이 자동으로 적용됩니다.
## 7.5 제품 ID 및 PCB 바코드 연동 예외 처리
## 7.4 타임아웃 처리
데이터 일관성과 무결성을 확보하기 위해 제품 식별 정보가 불완전한 상태에서의 데이터 저장을 차단하는 안전 검증 절차가 작동합니다.
Sentinel C28 센서에서 30초 이내에 응답이 없으면:
### 1. 제품 ID(IC_SN) 판독 실패 시
- **동작**: 제품 ID 판독 장치(4251 보드)로부터 16자리 ID 취득을 실패하는 경우 진행 메시지에 `"ID 읽기 실패"`를 표시하고, 해당 채널의 NG 물리 신호를 즉시 출력합니다.
- **저장 제한**: 이 경우에는 로컬 CSV 로그 기록 및 데이터베이스 저장을 **완전히 건너뜁니다**.
1. "Sentinel C28 통신 타임아웃" 오류 메시지 표시 (DB 저장 실패 시 "DB 저장 실패"도 오류 메시지에 추가 표시)
2. 해당 채널 DIO `NG` 출력 ON
3. 측정값 `0.0000`, 판정 `NG`, 센서판정 `T/O` (Timeout)로 기록
4. CSV 로그 파일 및 MSSQL 데이터베이스에 NG 결과 비동기 기록
### 2. PCB 바코드 DB 조회(매핑) 실패 시
- **동작**: 판독 완료 후 데이터베이스 서버에서 `IC_SN`에 해당하는 `PCB_Barcode`를 조회할 때, 정해진 시간(5초) 내에 응답이 없거나 일치하는 데이터가 없는 경우 화면에 `"바코드 매핑 실패"` 관련 오류를 표시하고 NG 신호를 출력합니다.
- **저장 제한**: 이 경우에도 제품 검사 자체를 진행하지 않고 즉시 스킵하며, **데이터베이스 및 로컬 CSV 로그 기록 저장을 건너뜁니다**. (오프라인 모드에서는 임시 바코드 정보를 주입하여 검사 동작을 테스트할 수 있습니다.)
## 7.5 C28 센서 결과 채널 라우팅
## 7.6 누설 센서 측정 결과 채널 분류
Sentinel C28 센서에서 수신한 데이터는 채널 번호(ChannelNo)로 구분됩니다:
누설 센서에서 수신한 데이터는 다음 채널 번호 정보를 기준으로 정렬됩니다.
| 채널 번호 | 대상 |
|----------|------|
| `C01` 또는 `1` | LEFT 채널 |
| `C02` 또는 `2` | RIGHT 채널 |
| 채널 번호 없음 | 현재 활성화된(시험 중인) 채널로 자동 할당 |
| `C01` 또는 `1` | 좌측(LEFT) 채널 |
| `C02` 또는 `2` | 우측(RIGHT) 채널 |
| 채널 번호 식별 불가 | 현재 활성화되어 시험을 진행 중인 채널로 자동 배정 |
## 7.6 ID 수동 테스트
## 7.7 ID 수동 테스트
하단 메뉴의 **ID Test (L)** 또는 **ID Test (R)** 버튼으로 DIO 시작 신호 없이 센서 ID 판독만 수행할 수 있습니다.
하단 메뉴의 **ID Test (L)** 또는 **ID Test (R)** 버튼을 사용하면 시험 시작 신호 없이 센서 ID 판독 동작만 단독으로 수행할 수 있습니다.
### 실행 흐름
1. 세마포어 잠금 획득 (다른 채널 시험 중이면 대기)
2. 결과 표시 초기화
3. 500ms 대기 (UI 피드백용)
4. "센서 정보 읽는 중" 메시지 표시
5. 4251 보드를 통해 제품 ID 판독
6. 성공 시: ID 표시 + "ID 테스트 완료"
7. 실패 시: ID를 "-"로 표시 + "ID 테스트 실패"
8. 세마포어 잠금 해제
1. 중복 방지 잠금 획득 (다른 채널이 시험 중인 경우 대기)
2. 결과 표시 초기화 및 0.5초 대기
3. "센서 정보 읽는 중" 메시지 표시 후 ID 판독 시작
4. 성공 시: 판독된 ID 표시 및 "ID 테스트 완료" 메시지 표시
5. 실패 시: ID 영역에 "-" 표시 및 "ID 테스트 실패" 메시지 표시
6. 잠금 해제
> **용도**: 설치 후 하드웨어 연결 상태 확인, 디버깅 시 활용합니다. LEAK 시험(3~6단계)은 수행하지 않습니다.
> **용도**: 장비 설치 후 통신 상태 확인 및 디버깅 시 사용합니다. 실제 누설 시험(압력 계측)은 수행하지 않습니다.
## 7.7 SPEC 교차 검증 및 판정 저장 기준
## 7.8 측정 판정 및 저장 기준
시험 과정에서 프로그램(UI) 판정과 C28 센서 자체의 판정이 불일치하는 경우, 데이터베이스 및 로그 저장의 기준은 다음과 같습니다.
프로그램의 판정 스펙(상하한 규격) 기준과 누설 센서 자체의 내부 스펙 판정 기준이 일치하지 않는 경우, 저장소에는 다음과 같이 저장됩니다.
### 저장 기준 요약
* **최종 저장 판정 (Result)**: C28 센서 자체의 판정값과 무관하게, 항상 **프로그램(UI) 판정 기준(`programJudgment`)**으로 데이터베이스 및 로그에 저장됩니다.
* **동작 예시**: UI 상으로 합격(`OK`)이지만 C28 센서 자체적으로는 불합격(`NG`/`R`)인 경우, 최종 DB(`Result` 컬럼) 및 CSV 로그에는 **`OK` (합격)**로 저장됩니다.
* **최종 저장 판정 (Result)**: 센서 자체의 판정값과 무관하게, 항상 **프로그램 화면 판정 기준**으로 데이터베이스 및 로그에 저장됩니다.
* **동작 예시**: 프로그램 판정이 합격(`OK`)이지만 센서 자체의 내부 판정이 불합격(`NG` 등)인 경우에도, 최종 기록 및 CSV에는 **`OK` (합격)**로 기록됩니다.
### 교차 검증 실패 시의 동작 (Spec Mismatch)
* 프로그램 판정과 센서 자체 판정이 다르면 `specMismatch` 플래그가 `true`로 설정됩니다.
* 불일치 발생 시 콘솔 로그에 경고(`[TestProcess] {side} SPEC Mismatch!`)를 출력하고 UI 완료 이벤트 인자로 전달되지만, **DB에 저장되는 최종 판정 결과(`Result`)에는 영향을 미치지 않고 프로그램 판정값을 유지**합니다.
### 판정 스펙 불일치 시의 동작
* 양측의 판정 스펙이 다르면 불일치 로그가 기록되며 화면에 경고 팝업이 출력되지만, **최종 저장되는 판정 결과에는 영향을 미치지 않고 프로그램 판정값을 유지**합니다.
<div class="page-break"></div>
@ -1126,7 +1162,7 @@ Sentinel C28 센서에서 수신한 데이터는 채널 번호(ChannelNo)로 구
| 항목 | 내용 |
|------|------|
| **저장 위치** | `C:\ProgramData\LeakTest\Logs\yyyy-MM-dd.csv` |
| **저장 위치** | `C:\LeakTest\Logs\yyyy-MM-dd.csv` |
| **파일 형식** | CSV (UTF-8 인코딩) |
| **파일 생성** | 날짜별 자동 생성 (해당 날짜 첫 시험 시) |
| **데이터 추가** | 매 시험 완료 시 자동으로 해당 날짜 파일에 1행 추가 |
@ -1165,8 +1201,6 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
> **참고**: 타임아웃 발생 시에도 측정값 `0.0000`, 판정 `NG`로 로그에 기록됩니다.
> **참고**: 프로그램은 구형 8개 컬럼 포맷(`Date,Time,ID,Channel,SpecUL,SpecLL,Value,Judgment`)의 로그 파일에 대해서도 컬럼 크기를 기준으로 자동 감지 및 파싱(하위 호환성)을 수행합니다.
> **참고**: CSV 값에 쉼표, 따옴표, 줄바꿈이 포함된 경우 자동으로 이스케이프 처리됩니다.
---
@ -1177,7 +1211,7 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
| 항목 | 내용 |
|------|------|
| **저장 위치** | `C:\ProgramData\LeakTest\Logs\yyyy-MM-dd_system.log` |
| **저장 위치** | `C:\LeakTest\Logs\yyyy-MM-dd_system.log` |
| **파일 형식** | 텍스트 파일 (UTF-8 인코딩) |
| **내용** | 통신 오류, 시스템 이벤트 등 |
@ -1201,7 +1235,7 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
| 항목 | 내용 |
|------|------|
| **위치** | 프로그램 실행 폴더 |
| **위치** | 프로그램 실행 폴더 (`C:\LeakTest\config.xml`) |
| **생성** | 최초 실행 시 기본값으로 자동 생성 |
| **수정** | Comm. Settings 또는 Parameters에서 저장 시 갱신 |
@ -1214,6 +1248,7 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
<Board4251Timeout>5000</Board4251Timeout>
<SpecUL>1.00</SpecUL>
<SpecLL>-1.00</SpecLL>
<EnableSpecMismatchAlarm>true</EnableSpecMismatchAlarm>
</AppConfig>
```
@ -1226,38 +1261,67 @@ IC_SN,PCB_Barcode,Maker,Model,Variant_1,Variant_2,Operator,Production_Date,Line,
| `Board4251Timeout` | 5000 | (코드 내 고정) | 4251 보드 응답 대기 시간(ms) |
| `SpecUL` | 1.00 | Parameters | 누설 상한 규격값 |
| `SpecLL` | -1.00 | Parameters | 누설 하한 규격값 |
| `EnableSpecMismatchAlarm` | true | Comm. Settings | 스펙 불일치 알림 활성화 여부 (true/false) |
### Database.ini
| 항목 | 내용 |
|------|------|
| **위치** | `C:\ProgramData\LeakTest\Database.ini` |
| **생성** | 배포 시 기본 포함 및 파일 누락 상태로 기동 시 기본 양식이 자동 생성됨 |
| **위치** | `C:\LeakTest\Database.ini` |
| **생성** | 최초 실행 시 해당 파일이 없으면 기본 양식으로 자동 생성. 배포 폴더(`C:\LeakTest\`)와 동일 위치에 저장되어 경로 관리가 단순함 |
| **수정** | 메모장 등의 텍스트 에디터를 통해 연결 주소 및 프로시저 정보 직접 수정 |
```ini
; =====================================================================
; Leak Test System - 데이터베이스 및 로그인 설정 파일 (Database.ini)
; =====================================================================
; [데이터베이스 접속 상세 설정]
Db_Server=192.168.0.20
Db_Name=NE1aW_PT_SENSOR
Db_User=sa
Db_Password=sa1234
; Db_Server : 중앙 MSSQL 데이터베이스 서버의 IP 주소 또는 호스트명
; Db_Name : 검사 데이터를 저장할 대상 데이터베이스 이름
; Db_User : 데이터베이스 접속 계정 ID (예: sa)
; Db_Password: 데이터베이스 접속 계정의 비밀번호
Db_Server=127.0.0.1, 1433
Db_Name=Test_DB
Db_User=db_user
Db_Password=db_password
[Login]
; Procedure: 로그인 검증에 사용할 DB 저장 프로시저명입니다. (공란일 경우 기본 테이블 조회 쿼리 작동)
Procedure=dbo.CheckOperator
; ProcessName: 로그인 검증 시 전달할 공정 이름입니다.
ProcessName=EOL
; OfflinePreview: 오프라인 모드 활성화 여부 (true = DB 연결 없이 로그인 통과, false = 실제 DB 인증)
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 | 로그인 계정 검증용 저장 프로시저명 |
| `Db_Server` | `[서버IP], [포트번호]` (예: 127.0.0.1, 1433) | 데이터베이스 서버 IP 주소 및 포트 번호 |
| `Db_Name` | `[데이터베이스명]` (예: Test_DB) | 대상 데이터베이스(DB) 이름 |
| `Db_User` | `[사용자ID]` (예: db_user) | 데이터베이스 접속 계정 ID |
| `Db_Password` | `[비밀번호]` (예: db_password) | 데이터베이스 접속 계정 비밀번호 |
| `Procedure` | dbo.CheckOperator | 로그인 계정 검증용 데이터베이스 프로그램명 |
| `ProcessName` | EOL | 검증 공정명 |
| `OfflinePreview` | false | true 시 DB 연결 없이 로그인 즉시 성공 (오프라인 모드) |
> **SSL 인증 오류 방지**: DB 연결 시 SSL 인증서 신뢰 오류가 발생하는 환경을 방지하기 위해 프로그램 내부에서 자동으로 `TrustServerCertificate=True;` 옵션을 연결 문자열 끝에 추가하여 안정적인 동작을 보장합니다.
### LoginOptions.json / LoginHistory.json
| 항목 | 내용 |
|------|------|
| **위치** | `C:\LeakTest\` |
| **생성** | 최초 실행 시 자동 생성 |
| **수정** | 프로그램 내 로그인 화면 사용 시 자동 갱신 |
| 파일 | 설명 |
|------|------|
| `LoginOptions.json` | 로그인 화면 각 필드(Maker, Model, Variant_1 등)의 자동완성 드롭다운 선택지 목록 저장 |
| `LoginHistory.json` | 최근 로그인 정보(작업자 ID, 라인, 모델 등 전체 세트)를 최대 10건 저장하여 재사용 |
### DioConfig.ini
| 항목 | 내용 |
@ -1302,33 +1366,33 @@ BoardNumber=0
---
## 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`) | |
## 8.5 데이터베이스 데이터 관리
검사가 완료되면 측정 결과와 작업 정보가 데이터베이스에 자동으로 저장됩니다.
### 데이터베이스 저장 항목 안내
* **저장 대상 테이블**: `EOL`
* **저장 항목 상세**:
| 데이터베이스 항목명 | 구분 | 설명 |
| :--- | :--- | :--- |
| **`IC_SN`** | 문자형 (최대 10자) | 제품 고유 IC 일련번호 |
| **`PCB_Barcode`** | 문자형 (최대 10자) | 조회된 PCB 바코드 정보 |
| `Maker` | 문자형 (최대 10자) | 로그인 시 입력한 제조사명 |
| `Model` | 문자형 (최대 10자) | 로그인 시 입력한 검사 모델명 |
| `Variant_1` | 문자형 (최대 10자) | 로그인 시 입력한 파생 모델명 1 |
| `Variant_2` | 문자형 (최대 10자) | 로그인 시 입력한 파생 모델명 2 |
| `Operator` | 문자형 (최대 10자) | 검사자(작업자) ID |
| `Production_Date` | 날짜/시간형 | 검사가 완료된 시간 |
| `Line` | 문자형 (최대 30자) | 로그인 시 입력한 작업 라인명 |
| `Lot_No` | 문자형 (최대 8자) | 로그인 시 입력한 Lot 번호 |
| `Jig_No` | 문자형 (최대 10자) | 로그인 시 입력한 지그 번호 |
| `Channel` | 문자형 (최대 5자) | 검사가 수행된 채널 (LEFT / RIGHT) |
| `Spec_UL` | 숫자형 | 리크 판정 상한 기준값 |
| `Spec_LL` | 숫자형 | 리크 판정 하한 기준값 |
| `Leak_Value` | 숫자형 | 측정된 누설량 |
| `Result` | 문자형 (최대 5자) | 최종 판정 결과 (OK / NG) |
<div class="page-break"></div>
@ -1355,18 +1419,19 @@ BoardNumber=0
| 오류 메시지 | 원인 | 조치 방법 |
|------------|------|----------|
| `Sentinel C28 통신 타임아웃` | 30초 내 C28 센서 응답 없음 | ① 센서 전원 ON 확인 ② 시리얼 케이블 연결 확인 ③ BaudRate 설정 확인 (기본: 9600) |
| `통신 실패 (4251 보드 CHx 상태 타임아웃)` | 4251 보드가 응답 시간(15초) 내에 상태 응답을 회신하지 않음 | ① 4251 보드 전원 공급 및 케이블 연결선 확인 ② 통신 노이즈 방지 조치 확인 |
| `통신 실패 (4251 보드 CHx 상태 응답 시간 초과)` | 4251 보드가 대기 시간(15초) 내에 응답하지 않음 | ① 4251 보드 전원 공급 및 케이블 연결선 확인 ② 통신 노이즈 방지 조치 확인 |
| `통신 실패 (4251 보드 CHx 상태 이상 또는 Fail)` | 4251 보드로부터 정상 응답을 받지 못했거나 오류 코드가 반환됨 | ① 하드웨어 오결선 여부 점검 ② 채널 센서 구동 전원 확인 |
| `통신 실패 (4251 보드 CHx ID 대기 타임아웃)` | 4251 보드를 통해 제품 ID를 15초 내에 판독하지 못함 | ① 센서가 지그(Jig)에 올바르게 안착되었는지 물리적 확인 ② 접촉 단자 이물질 청소 |
| `SPEC 불일치 - 프로그램: OK, 센서: R` | 프로그램 SPEC과 센서 자체 SPEC의 판정이 다름 | ① 프로그램의 SPEC UL/LL 확인 (Parameters) ② C28 센서의 SPEC 설정 확인 ③ 양쪽 설정값 동기화 |
| `SPEC 불일치 - 프로그램: OK, 센서: R` | 프로그램 SPEC과 센서 자체 SPEC의 판정이 다름 | ① 프로그램의 SPEC UL/LL 확인 (Parameters) ② C28 센서의 SPEC 설정 확인 ③ 양쪽 설정값 동기화 (※ 통신 설정에서 경고 알림 스위치 상태를 확인하세요) |
| `LEAK 센서 데이터 오류` | C28에서 수신된 데이터 파싱 실패 | ① 센서 통신 상태 확인 ② 케이블 접촉 불량 확인 ③ BaudRate 불일치 확인 |
| `DB 저장 실패` | 중앙 MSSQL DB 서버에 검사 데이터를 저장하지 못함 | ① PC 네트워크 케이블 및 통신 연결 확인 ② Database.ini 파일의 접속 정보(IP/비밀번호 등) 확인 ③ 로컬 일일 CSV 로그 파일(`C:\ProgramData\LeakTest\Logs/yyyy-MM-dd.csv`)에 백업이 잘 저장되었는지 확인 |
| `DB 저장 실패: [오류 원인] 상세 메시지` | 데이터베이스 서버에 검사 데이터를 저장하지 못함 | ① 네트워크 케이블 및 서버 통신 연결 확인 ② Database.ini 설정 파일의 접속 정보(IP/비밀번호 등) 확인 ③ 로컬 일일 CSV 로그 파일에 백업이 정상적으로 저장되었는지 확인 |
| `바코드 매핑 실패: [오류 원인] 상세 메시지` | 데이터베이스에서 해당 `IC_SN`에 매칭되는 바코드 정보를 찾지 못함 | ① 데이터베이스 서버 네트워크 연결 확인 ② Housing_Assembly 테이블에 대상 제품의 바코드 정보 매핑 상태 점검 |
---
## 9.3 SPEC 교차 검증 경고 팝업
프로그램 판정과 센서 판정이 불일치하면 팝업 경고가 표시됩니다:
프로그램 판정과 센서 판정이 불일치할 때, 통신 설정 화면의 **`스펙 불일치 알림 활성화`** 옵션이 켜져(ON) 있는 경우 팝업 경고가 표시됩니다:
```
[!] SPEC 교차 검증 경고
@ -1374,7 +1439,8 @@ BoardNumber=0
[확인]
```
이 경고는 검사 자체를 중단하지는 않지만, **프로그램의 판정 결과**가 최종 기록에 사용됩니다.
- 만약 이 옵션을 **해제(OFF)**한 경우, 이 팝업 경고 대화상자는 나타나지 않으며, 메인 화면 하단의 **오류 메시지** 텍스트 박스에만 불일치 로그가 기록됩니다.
- 이 경고는 검사 자체를 중단하지는 않으며, **프로그램의 판정 결과**가 최종 DB 및 로컬 로그 기록에 사용됩니다.
---
@ -1385,7 +1451,7 @@ BoardNumber=0
| PCI-7432 보드 미인식 | **시뮬레이션 모드**로 자동 전환 | DIO 입출력이 동작하지 않음 (실제 입출력 포트 제어 불가) |
| 보드 런타임 오류 | 좌/우 채널 오류 메시지에 `[DIO Board Error]` 표시 | 오류 내용에 따라 조치 |
> **시뮬레이션 모드**: ADLINK 드라이버가 신설되지 않은 개발 환경에서도 프로그램의 나머지 기능(통신 설정, 데이터 조회 등)을 테스트할 수 있도록 자동 전환됩니다.
> **시뮬레이션 모드**: ADLINK 드라이버가 설치되지 않은 환경에서도 프로그램의 기능(통신 설정, 데이터 조회 등)을 사전에 테스트할 수 있도록 제공되는 가상 동작 모드입니다.
---
@ -1423,9 +1489,9 @@ BoardNumber=0
| 순서 | 처리 내용 |
|------|----------|
| 1 | 시험 프로세스 스레드 종료 (좌/우 모두) |
| 2 | 대기 중인 세마포어 해제 |
| 3 | DIO 이벤트 구독 해제 |
| 4 | C28 센서 이벤트 구독 해제 |
| 2 | 대기 중인 순서 잠금 해제 |
| 3 | 디지털 입출력(DIO) 신호 감지 중단 |
| 4 | 누설 센서 신호 감지 중단 |
| 5 | Sentinel C28 시리얼 통신 연결 해제 |
| 6 | 4251 보드 시리얼 통신 연결 해제 |
| 7 | DIO `PC_ON` 출력 **OFF** |

4
leak_test_project/Manual/사용자_메뉴얼.md

@ -1,6 +1,6 @@
# Pressure Leak Inspect System - 사용자 메뉴얼
> **버전**: 1.0.0 | **최종 수정**: 2026-06-25
> **버전**: 1.0.0 | **최종 수정**: 2026-07-01
---
@ -27,7 +27,7 @@
처음 사용하는 경우 아래 순서를 따르세요:
1. **하드웨어 연결 확인** → [시스템 개요 및 요구사항](01_시스템_개요.md) 참조
2. **프로그램 실행 및 로그인**`leak_test_project.exe` 실행 후 로그인 완료 (오프라인 테스트는 `Database.ini`에서 `OfflinePreview=true` 설정)
2. **프로그램 실행 및 로그인**바탕화면의 **바로가기 아이콘**을 실행하여 로그인 완료 (오프라인 테스트는 `Database.ini`에서 `OfflinePreview=true` 설정)
3. **통신 설정** → 하단 메뉴 `Comm. Settings` → COM 포트 선택 → `설정 저장`
4. **시험 설정** → 하단 메뉴 `Parameters` → SPEC UL/LL 입력 → `설정 저장`
5. **시험 시작** → DIO 시작 신호 또는 `ID Test (L/R)` 버튼으로 동작 확인

5
leak_test_project/Models/ProjectModels.cs

@ -16,6 +16,11 @@ namespace leak_test_project.Models
public double SpecUL { get; set; } = 1.00;
public double SpecLL { get; set; } = -1.00;
/// <summary>
/// 스펙 불일치 알림 활성화 여부
/// </summary>
public bool EnableSpecMismatchAlarm { get; set; } = true;
/// <summary>
/// MSSQL 데이터베이스 연결 문자열 (더 이상 사용되지 않음. 대신 Database.ini의 설정을 읽어 접속합니다.)
/// </summary>

2
leak_test_project/Properties/PublishProfiles/ClickOnceProfile.pubxml

@ -16,7 +16,7 @@
<OpenBrowserOnPublish>False</OpenBrowserOnPublish>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net9.0-windows\win-x86\app.publish\</PublishDir>
<PublishUrl>C:\Users\COMPUTER1\Downloads\</PublishUrl>
<PublishUrl>C:\LeakTest\</PublishUrl>
<PublishProtocol>ClickOnce</PublishProtocol>
<PublishReadyToRun>False</PublishReadyToRun>
<PublishSingleFile>True</PublishSingleFile>

4
leak_test_project/Properties/PublishProfiles/FolderProfile.pubxml

@ -1,10 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>C:\Users\COMPUTER1\Downloads</PublishDir>
<PublishDir>C:\Users\COMPUTER1\Downloads\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<_TargetId>Folder</_TargetId>
<TargetFramework>net9.0-windows</TargetFramework>

2
leak_test_project/Properties/PublishProfiles/FolderProfile.pubxml.user

@ -2,7 +2,7 @@
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<History>True|2026-07-02T02:10:03.3205130Z||;True|2026-07-02T10:57:07.7675289+09:00||;True|2026-07-02T10:48:02.4094102+09:00||;True|2026-07-02T10:47:32.3161255+09:00||;True|2026-07-02T09:56:10.5544859+09:00||;True|2026-07-01T16:42:19.4986373+09:00||;True|2026-07-01T16:30:40.3015254+09:00||;True|2026-07-01T16:15:05.1946766+09:00||;True|2026-07-01T16:14:42.7624440+09:00||;True|2026-07-01T16:10:15.7662884+09:00||;True|2026-07-01T11:40:58.7303122+09:00||;True|2026-07-01T10:52:14.8628105+09:00||;True|2026-07-01T10:45:31.8793742+09:00||;True|2026-07-01T10:35:19.6809435+09:00||;True|2026-07-01T10:35:00.9643317+09:00||;True|2026-07-01T10:32:31.0544609+09:00||;True|2026-07-01T10:25:33.3316628+09:00||;True|2026-07-01T10:23:02.2841540+09:00||;True|2026-07-01T10:19:59.2997126+09:00||;False|2026-07-01T10:17:21.1004186+09:00||;False|2026-06-30T10:32:04.0883654+09:00||;True|2026-06-30T10:24:54.7745409+09:00||;True|2026-06-25T14:28:15.0458856+09:00||;True|2026-06-25T13:57:36.3233571+09:00||;True|2026-06-25T12:56:04.8828587+09:00||;</History>
<History>True|2026-07-24T02:09:35.2227587Z||;True|2026-07-24T11:05:48.8845478+09:00||;True|2026-07-24T11:05:14.3843421+09:00||;True|2026-07-24T11:04:27.7256207+09:00||;True|2026-07-24T11:00:36.3011595+09:00||;True|2026-07-24T10:55:51.6585002+09:00||;True|2026-07-24T10:55:02.4837763+09:00||;True|2026-07-24T10:50:36.6197914+09:00||;True|2026-07-24T10:49:58.5199665+09:00||;True|2026-07-24T10:49:17.2856270+09:00||;True|2026-07-24T10:48:16.6349673+09:00||;True|2026-07-24T10:45:17.3653596+09:00||;True|2026-07-24T10:41:32.2281978+09:00||;True|2026-07-24T10:39:09.8181835+09:00||;True|2026-07-24T10:30:36.6348545+09:00||;True|2026-07-24T10:28:51.9168366+09:00||;True|2026-07-24T10:24:29.6224638+09:00||;True|2026-07-21T10:52:14.1396563+09:00||;True|2026-07-21T10:51:15.9786396+09:00||;True|2026-07-21T10:49:23.8272985+09:00||;True|2026-07-21T10:43:12.2452400+09:00||;True|2026-07-02T11:10:03.3205130+09:00||;True|2026-07-02T10:57:07.7675289+09:00||;True|2026-07-02T10:48:02.4094102+09:00||;True|2026-07-02T10:47:32.3161255+09:00||;True|2026-07-02T09:56:10.5544859+09:00||;True|2026-07-01T16:42:19.4986373+09:00||;True|2026-07-01T16:30:40.3015254+09:00||;True|2026-07-01T16:15:05.1946766+09:00||;True|2026-07-01T16:14:42.7624440+09:00||;True|2026-07-01T16:10:15.7662884+09:00||;True|2026-07-01T11:40:58.7303122+09:00||;True|2026-07-01T10:52:14.8628105+09:00||;True|2026-07-01T10:45:31.8793742+09:00||;True|2026-07-01T10:35:19.6809435+09:00||;True|2026-07-01T10:35:00.9643317+09:00||;True|2026-07-01T10:32:31.0544609+09:00||;True|2026-07-01T10:25:33.3316628+09:00||;True|2026-07-01T10:23:02.2841540+09:00||;True|2026-07-01T10:19:59.2997126+09:00||;False|2026-07-01T10:17:21.1004186+09:00||;False|2026-06-30T10:32:04.0883654+09:00||;True|2026-06-30T10:24:54.7745409+09:00||;True|2026-06-25T14:28:15.0458856+09:00||;True|2026-06-25T13:57:36.3233571+09:00||;True|2026-06-25T12:56:04.8828587+09:00||;</History>
<LastFailureDetails />
</PropertyGroup>
</Project>

89
leak_test_project/Services/Board4251.cs

@ -20,6 +20,7 @@ namespace leak_test_project.Services
private readonly StringBuilder _receiveBuffer = new StringBuilder();
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private TaskCompletionSource<string> _responseTcs;
private readonly object _tcsLock = new object(); // TCS 동기화용 락 객체
public int TimeoutMs { get; set; } = 5000; // 보드가 응답을 주는데 2초 이상 걸리므로 무조건 길게 대기
private bool _shouldBeConnected = false;
private System.Timers.Timer _reconnectTimer;
@ -39,10 +40,7 @@ namespace leak_test_project.Services
{
_communication = communication;
_communication.DataReceived += OnDataReceived;
_communication.ConnectionStatusChanged += (s, isConnected) => {
ConnectionChanged?.Invoke(this, isConnected);
if (!isConnected && _shouldBeConnected) StartReconnectTimer();
};
_communication.ConnectionStatusChanged += OnConnectionStatusChanged;
_reconnectTimer = new System.Timers.Timer(1000);
_reconnectTimer.AutoReset = false;
@ -61,6 +59,12 @@ namespace leak_test_project.Services
};
}
private void OnConnectionStatusChanged(object sender, bool isConnected)
{
ConnectionChanged?.Invoke(this, isConnected);
if (!isConnected && _shouldBeConnected) StartReconnectTimer();
}
private void StartReconnectTimer()
{
if (_reconnectTimer != null && !_reconnectTimer.Enabled) _reconnectTimer.Start();
@ -85,9 +89,13 @@ namespace leak_test_project.Services
/// 4251 보드의 상태를 확인합니다.
/// </summary>
/// <returns>성공 여부</returns>
public async Task<bool> CheckStatusAsync(int channel = 1)
/// <summary>
/// 4251 보드의 상태를 확인합니다.
/// </summary>
/// <returns>성공 여부</returns>
public async Task<bool> CheckStatusAsync(int channel = 1, CancellationToken cancellationToken = default)
{
string response = await SendCommandAsync($"x00c_00{channel}101:owt28006727ea97c7801\r\n");
string response = await SendCommandAsync($"x00c_00{channel}101:owt28006727ea97c7801\r\n", cancellationToken);
if (response == null)
{
@ -112,9 +120,9 @@ namespace leak_test_project.Services
/// 4251 보드로부터 16자리 ID를 읽어옵니다.
/// </summary>
/// <returns>16자리 ID 문자열, 실패 시 null</returns>
public async Task<string> ReadIdAsync(int channel = 1)
public async Task<string> ReadIdAsync(int channel = 1, CancellationToken cancellationToken = default)
{
string response = await SendCommandAsync($"x00c_00{channel}101:ow2800326003e\r\n");
string response = await SendCommandAsync($"x00c_00{channel}101:ow2800326003e\r\n", cancellationToken);
if (response == null)
{
@ -156,47 +164,61 @@ namespace leak_test_project.Services
return null;
}
public async Task<string> SendCommandAsync(string command)
public async Task<string> SendCommandAsync(string command, CancellationToken cancellationToken = default)
{
await _lock.WaitAsync();
await _lock.WaitAsync(cancellationToken);
try
{
int retryCount = 0;
while (retryCount <= 3)
{
cancellationToken.ThrowIfCancellationRequested();
if (!_communication.IsOpen)
{
if (!_communication.Open())
{
retryCount++;
await Task.Delay(300);
await Task.Delay(300, cancellationToken);
continue;
}
}
_receiveBuffer.Clear();
LastResponse = "";
_responseTcs = new TaskCompletionSource<string>();
TaskCompletionSource<string> tcs;
lock (_tcsLock)
{
_responseTcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
tcs = _responseTcs;
}
_communication.ClearBuffer(); // 이전에 남아있던 패킷 조각 완벽히 제거
Debug.WriteLine($"[Board4251] Sending Command: {command.TrimEnd()}");
_communication.Write(command);
using (var cts = new CancellationTokenSource(TimeoutMs))
using (var timeoutCts = new CancellationTokenSource(TimeoutMs))
using (var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token))
{
cts.Token.Register(() => _responseTcs.TrySetCanceled());
try
using (linkedCts.Token.Register(() => tcs.TrySetCanceled()))
{
return await _responseTcs.Task;
}
catch (OperationCanceledException)
{
retryCount++;
string currentBuffer = _receiveBuffer.ToString().Trim();
FileLogger.Log("WARNING", $"[Board4251] Timeout waiting for response (Retry {retryCount}/3). Command: {command.Trim()}, ReceivedSoFar: {currentBuffer}");
_receiveBuffer.Clear();
if (retryCount <= 3) await Task.Delay(300);
try
{
return await tcs.Task;
}
catch (OperationCanceledException)
{
// 외부 취소 요청에 의해 중단된 경우 그대로 버블링
cancellationToken.ThrowIfCancellationRequested();
retryCount++;
string currentBuffer = _receiveBuffer.ToString().Trim();
FileLogger.Log("WARNING", $"[Board4251] Timeout waiting for response (Retry {retryCount}/3). Command: {command.Trim()}, ReceivedSoFar: {currentBuffer}");
_receiveBuffer.Clear();
if (retryCount <= 3) await Task.Delay(300, cancellationToken);
}
}
}
}
@ -208,7 +230,10 @@ namespace leak_test_project.Services
}
finally
{
_responseTcs = null;
lock (_tcsLock)
{
_responseTcs = null;
}
_lock.Release();
}
}
@ -248,10 +273,13 @@ namespace leak_test_project.Services
if (isComplete)
{
if (_responseTcs != null && !_responseTcs.Task.IsCompleted)
lock (_tcsLock)
{
LastResponse = currentContent.Trim();
_responseTcs.TrySetResult(currentContent);
if (_responseTcs != null && !_responseTcs.Task.IsCompleted)
{
LastResponse = currentContent.Trim();
_responseTcs.TrySetResult(currentContent);
}
}
_receiveBuffer.Clear();
}
@ -261,6 +289,7 @@ namespace leak_test_project.Services
{
Disconnect();
_communication.DataReceived -= OnDataReceived;
_communication.ConnectionStatusChanged -= OnConnectionStatusChanged;
_reconnectTimer?.Dispose();
_lock.Dispose();
}
@ -310,7 +339,7 @@ namespace leak_test_project.Services
using (var cts = new CancellationTokenSource(extendedTimeout))
{
var statusTask = _service.CheckStatusAsync(channel);
var statusTask = _service.CheckStatusAsync(channel, cts.Token);
var delayTask = Task.Delay(extendedTimeout, cts.Token);
var completedTask = await Task.WhenAny(statusTask, delayTask);
if (completedTask == delayTask)
@ -341,7 +370,7 @@ namespace leak_test_project.Services
{
using (var cts = new CancellationTokenSource(extendedTimeout))
{
var idTask = _service.ReadIdAsync(channel);
var idTask = _service.ReadIdAsync(channel, cts.Token);
var delayTask = Task.Delay(extendedTimeout, cts.Token);
var completedTask = await Task.WhenAny(idTask, delayTask);
if (completedTask == delayTask)

107
leak_test_project/Services/SentinelC28Service.cs

@ -33,10 +33,7 @@ namespace leak_test_project.Services
{
_communication = communication;
_communication.DataReceived += OnDataReceived;
_communication.ConnectionStatusChanged += (s, isConnected) => {
ConnectionChanged?.Invoke(this, isConnected);
if (!isConnected && _shouldBeConnected) StartReconnectTimer();
};
_communication.ConnectionStatusChanged += OnConnectionStatusChanged;
// 1초(1000ms)마다 연결 상태를 확인하고 재연결 시도
_reconnectTimer = new Timer(1000);
@ -59,6 +56,12 @@ namespace leak_test_project.Services
};
}
private void OnConnectionStatusChanged(object sender, bool isConnected)
{
ConnectionChanged?.Invoke(this, isConnected);
if (!isConnected && _shouldBeConnected) StartReconnectTimer();
}
public bool Connect()
{
_shouldBeConnected = true;
@ -103,7 +106,8 @@ namespace leak_test_project.Services
}
}
private string _receiveBuffer = string.Empty;
private readonly System.Text.StringBuilder _receiveBuffer = new System.Text.StringBuilder(8192);
private const int MaxBufferSize = 65536; // 64KB
private void OnDataReceived(object sender, string rawData)
{
@ -113,47 +117,50 @@ namespace leak_test_project.Services
if (string.IsNullOrEmpty(rawData)) return;
_receiveBuffer += rawData;
// \n 또는 \r을 기준으로 프레임을 나눔
while (_receiveBuffer.Contains("\n") || _receiveBuffer.Contains("\r"))
lock (_receiveBuffer)
{
int indexN = _receiveBuffer.IndexOf('\n');
int indexR = _receiveBuffer.IndexOf('\r');
int splitIndex = -1;
if (indexN >= 0 && indexR >= 0) splitIndex = Math.Min(indexN, indexR);
else if (indexN >= 0) splitIndex = indexN;
else splitIndex = indexR;
string frame = _receiveBuffer.Substring(0, splitIndex);
// 버퍼 갱신 (처리한 구분자까지 잘라냄)
_receiveBuffer = _receiveBuffer.Substring(splitIndex + 1);
// 빈 문자열이거나 단순히 \r\n 이 연속되어 발생하는 빈 프레임이면 무시
if (string.IsNullOrWhiteSpace(frame)) continue;
if (_receiveBuffer.Length + rawData.Length > MaxBufferSize)
{
_receiveBuffer.Clear();
FileLogger.Log("WARNING", "[SentinelC28] Buffer size limit exceeded. Buffer cleared to prevent OOM.");
}
// 헤더 분석 및 본문 추출
string body = SentinelParser.ExtractBody(frame, out char typeCode);
_receiveBuffer.Append(rawData);
string content = _receiveBuffer.ToString();
switch (typeCode)
int splitIndex;
while ((splitIndex = GetFirstDelimiterIndex(content)) >= 0)
{
case 'R': // 최종 결과 (Result Value)
ResultReceived?.Invoke(this, frame);
var finalParsed = SentinelParser.ParseFinalResult(frame);
OnFinalResultParsed?.Invoke(this, finalParsed);
break;
case 'S': // 스트리밍 데이터 (Streaming Value)
StreamingReceived?.Invoke(this, frame);
var streamParsed = SentinelParser.ParseStreamingValue(frame);
OnStreamingParsed?.Invoke(this, streamParsed);
break;
case 'M': // 일반 메시지
FileLogger.Log("INFO", $"[SentinelC28 Message] {body}");
break;
string frame = content.Substring(0, splitIndex);
content = content.Substring(splitIndex + 1);
// 버퍼 갱신 (처리한 데이터 잘라냄)
_receiveBuffer.Clear();
_receiveBuffer.Append(content);
if (string.IsNullOrWhiteSpace(frame)) continue;
// 헤더 분석 및 본문 추출
string body = SentinelParser.ExtractBody(frame, out char typeCode);
switch (typeCode)
{
case 'R': // 최종 결과 (Result Value)
ResultReceived?.Invoke(this, frame);
var finalParsed = SentinelParser.ParseFinalResult(frame);
OnFinalResultParsed?.Invoke(this, finalParsed);
break;
case 'S': // 스트리밍 데이터 (Streaming Value)
StreamingReceived?.Invoke(this, frame);
var streamParsed = SentinelParser.ParseStreamingValue(frame);
OnStreamingParsed?.Invoke(this, streamParsed);
break;
case 'M': // 일반 메시지
FileLogger.Log("INFO", $"[SentinelC28 Message] {body}");
break;
}
}
}
} catch (Exception ex) {
@ -161,9 +168,25 @@ namespace leak_test_project.Services
}
}
private int GetFirstDelimiterIndex(string text)
{
int indexN = text.IndexOf('\n');
int indexR = text.IndexOf('\r');
if (indexN >= 0 && indexR >= 0) return Math.Min(indexN, indexR);
return indexN >= 0 ? indexN : indexR;
}
public void Dispose()
{
Disconnect();
// [Memory Leak 방지] 이벤트 핸들러 명시적 구독 해제
if (_communication != null)
{
_communication.DataReceived -= OnDataReceived;
_communication.ConnectionStatusChanged -= OnConnectionStatusChanged;
}
_reconnectTimer?.Stop();
_reconnectTimer?.Dispose();
_reconnectTimer = null;

507
leak_test_project/Services/TestProcessService.cs

@ -52,6 +52,7 @@ namespace leak_test_project.Services
private volatile bool _rightTestStart = false;
private volatile bool _running = true;
private readonly SemaphoreSlim _testLock = new SemaphoreSlim(1, 1);
private readonly SemaphoreSlim _dbSemaphore = new SemaphoreSlim(3, 3); // DB 저장 동시성 제한용 세마포어 (백프레셔)
// DIO 시작 신호를 폴링 대신 대기하기 위한 이벤트
private readonly AutoResetEvent _leftStartEvent = new AutoResetEvent(false);
@ -218,96 +219,129 @@ namespace leak_test_project.Services
while (_running)
{
// 이벤트 신호 대기 (2ms 폴링 대신 신호 수신 시까지 차단)
startEvent.WaitOne();
if (!_running) break;
// 시작 신호 대기
bool shouldStart = isLeft ? _leftTestStart : _rightTestStart;
if (!shouldStart) continue;
// 순차 실행을 위한 락 획득 (좌/우 동시 테스트 방지)
// 순차 실행을 위한 락 획득 (좌/우 동시 테스트 방지)
bool lockAcquired = _testLock.Wait(0);
if (!lockAcquired)
{
ResultClearRequested?.Invoke(this, testIndex);
NotifyProgress(testIndex, "대기중", "LightYellow");
_testLock.Wait();
}
try
{
_activeTestIndex = testIndex;
// 하드웨어 통신 안정화를 위한 짧은 대기 (이전 채널 종료 후 여유 시간)
Thread.Sleep(350);
// 플래그 초기화
if (isLeft) _leftTestStart = false;
else _rightTestStart = false;
NotifyProgress(testIndex, "시험 시작", "LightSeaGreen");
ResultClearRequested?.Invoke(this, testIndex);
// C28 LEAK 센서 수신 이벤트를 초기화 (제품 ID 통신 중 일찍 날아오는 Auto Result를 놓치지 않기 위함)
var resultReady = isLeft ? _leftResultReady : _rightResultReady;
resultReady.Reset();
if (isLeft) _leftResult = null;
else _rightResult = null;
// === 1단계: 센서 정보 읽기 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 1: 센서 정보 읽기 시작");
NotifyProgress(testIndex, "센서 정보 읽는 중", "LightSeaGreen");
var sensor = isLeft ? _leftSensor : _rightSensor;
var sensorData = sensor.ReadSensorAsync().GetAwaiter().GetResult();
if (sensorData != null)
{
SensorReadComplete?.Invoke(this, (testIndex, sensorData));
}
else
{
NotifyError(testIndex, "ID 읽기 실패");
_dioBoard.WriteOutput(side + "_NG", true);
// 이벤트 신호 대기 (2ms 폴링 대신 신호 수신 시까지 차단)
startEvent.WaitOne();
var failedSensorData = new SensorIdData { ID = "-" };
SensorReadComplete?.Invoke(this, (testIndex, failedSensorData));
if (!_running) break;
TestCompleted?.Invoke(this, new TestResultEventArgs
{
TestIndex = testIndex,
SensorData = failedSensorData,
PcbBarcode = "-",
MeasuredValue = "0.0000",
Judgment = "NG",
SensorJudgment = "FAIL",
SpecMismatch = false,
ProductionDate = DateTime.Now
});
// 시작 신호 대기
bool shouldStart = isLeft ? _leftTestStart : _rightTestStart;
if (!shouldStart) continue;
continue;
}
// === 1.5단계: PCB 바코드 DB 조회 (dbo.Housing_Assembly) ===
string pcbBarcode = null;
var dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath();
var loginSettings = Housing.Services.LoginSettings.Load(dbIniPath);
if (loginSettings.OfflinePreview)
// 순차 실행을 위한 락 획득 (좌/우 동시 테스트 방지)
bool lockAcquired = _testLock.Wait(0);
if (!lockAcquired)
{
pcbBarcode = "testPCBbarcode";
ResultClearRequested?.Invoke(this, testIndex);
NotifyProgress(testIndex, "대기중", "LightYellow");
_testLock.Wait();
}
else
try
{
_activeTestIndex = testIndex;
// 하드웨어 통신 안정화를 위한 짧은 대기 (이전 채널 종료 후 여유 시간)
Thread.Sleep(350);
// 플래그 초기화
if (isLeft) _leftTestStart = false;
else _rightTestStart = false;
NotifyProgress(testIndex, "시험 시작", "LightSeaGreen");
ResultClearRequested?.Invoke(this, testIndex);
// C28 LEAK 센서 수신 이벤트를 초기화 (제품 ID 통신 중 일찍 날아오는 Auto Result를 놓치지 않기 위함)
var resultReady = isLeft ? _leftResultReady : _rightResultReady;
resultReady.Reset();
if (isLeft) _leftResult = null;
else _rightResult = null;
// === 1단계: 센서 정보 읽기 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 1: 센서 정보 읽기 시작");
NotifyProgress(testIndex, "센서 정보 읽는 중", "LightSeaGreen");
var sensor = isLeft ? _leftSensor : _rightSensor;
// Sync-over-Async 예방을 위해 Task.Run을 활용하여 백그라운드 태스크로 호출 및 대기
SensorIdData sensorData = null;
try
{
var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
var dbService = new EolDatabaseService(dbConnStr);
pcbBarcode = dbService.GetPcbBarcodeAsync(sensorData.ID).GetAwaiter().GetResult();
sensorData = Task.Run(async () => await sensor.ReadSensorAsync()).GetAwaiter().GetResult();
}
catch (Exception ex)
{
string detailedErr = $"바코드 매핑 실패: [{ex.GetType().Name}] {ex.Message}";
NotifyError(testIndex, detailedErr);
FileLogger.Log("ERROR", $"[TestProcess-{side}] ID Sensor Read Exception: {ex.Message}");
}
if (sensorData == null)
{
NotifyError(testIndex, "ID 읽기 실패");
_dioBoard.WriteOutput(side + "_NG", true);
var failedSensorData = new SensorIdData { ID = "-" };
SensorReadComplete?.Invoke(this, (testIndex, failedSensorData));
TestCompleted?.Invoke(this, new TestResultEventArgs
{
TestIndex = testIndex,
SensorData = failedSensorData,
PcbBarcode = "-",
MeasuredValue = "0.0000",
Judgment = "NG",
SensorJudgment = "FAIL",
SpecMismatch = false,
ProductionDate = DateTime.Now
});
continue;
}
SensorReadComplete?.Invoke(this, (testIndex, sensorData));
// === 1.5단계: PCB 바코드 DB 조회 (dbo.Housing_Assembly) ===
string pcbBarcode = null;
var dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath();
var loginSettings = Housing.Services.LoginSettings.Load(dbIniPath);
if (loginSettings.OfflinePreview)
{
pcbBarcode = "testPCBbarcode";
}
else
{
try
{
var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
var dbService = new EolDatabaseService(dbConnStr);
// Sync-over-Async 예방 적용
pcbBarcode = Task.Run(async () => await dbService.GetPcbBarcodeAsync(sensorData.ID)).GetAwaiter().GetResult();
}
catch (Exception ex)
{
string detailedErr = $"바코드 매핑 실패: [{ex.GetType().Name}] {ex.Message}";
NotifyError(testIndex, detailedErr);
_dioBoard.WriteOutput(side + "_NG", true);
var failedSensorData = new SensorIdData { ID = sensorData.ID };
TestCompleted?.Invoke(this, new TestResultEventArgs
{
TestIndex = testIndex,
SensorData = failedSensorData,
PcbBarcode = "-",
MeasuredValue = "0.0000",
Judgment = "NG",
SensorJudgment = "FAIL",
SpecMismatch = false,
ProductionDate = DateTime.Now
});
NotifyProgress(testIndex, "시험 완료", "LightBlue");
continue;
}
}
if (string.IsNullOrEmpty(pcbBarcode))
{
NotifyError(testIndex, "바코드 매핑 실패: 매핑 데이터 없음");
_dioBoard.WriteOutput(side + "_NG", true);
var failedSensorData = new SensorIdData { ID = sensorData.ID };
@ -325,59 +359,131 @@ namespace leak_test_project.Services
NotifyProgress(testIndex, "시험 완료", "LightBlue");
continue;
}
}
if (string.IsNullOrEmpty(pcbBarcode))
{
NotifyError(testIndex, "바코드 매핑 실패: 매핑 데이터 없음");
_dioBoard.WriteOutput(side + "_NG", true);
// 정상 매핑 시점 이벤트 발생
BarcodeMapped?.Invoke(this, (testIndex, sensorData.ID, pcbBarcode));
var failedSensorData = new SensorIdData { ID = sensorData.ID };
TestCompleted?.Invoke(this, new TestResultEventArgs
// === 3단계: LEAK 시험 수행 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 3: LEAK 시험 대기 시작 (최대 30초)");
NotifyProgress(testIndex, "LEAK 시험중", "LightYellow");
// C28에서 결과를 수신할 때까지 대기 (레거시와 동일하게 30초 설정)
bool received = resultReady.Wait(TimeSpan.FromSeconds(30));
if (!received)
{
TestIndex = testIndex,
SensorData = failedSensorData,
PcbBarcode = "-",
MeasuredValue = "0.0000",
Judgment = "NG",
SensorJudgment = "FAIL",
SpecMismatch = false,
ProductionDate = DateTime.Now
});
NotifyProgress(testIndex, "시험 완료", "LightBlue");
continue;
}
Debug.WriteLine($"[TestProcess-{side}] ERROR: Sentinel C28 통신 타임아웃 발생!");
NotifyError(testIndex, "Sentinel C28 통신 타임아웃");
_dioBoard.WriteOutput(side + "_NG", true);
// 타임아웃 시에도 UI(그리드)에 NG로 기록되도록 이벤트 발생
TestCompleted?.Invoke(this, new TestResultEventArgs
{
TestIndex = testIndex,
SensorData = sensorData,
MeasuredValue = "9.9999",
Judgment = "NG",
SensorJudgment = "T/O", // Timeout
SpecMismatch = false,
ProductionDate = DateTime.Now
});
// 정상 매핑 시점 이벤트 발생
BarcodeMapped?.Invoke(this, (testIndex, sensorData.ID, pcbBarcode));
// 로그 파일에도 기록
var timeoutData = new InspectData
{
InspectDate = DateTime.Now.ToString("yyyy-MM-dd"),
InspectTime = DateTime.Now.ToString("HH:mm:ss"),
Channel = side,
ProductId = sensorData.ID,
IcSn = sensorData.ID,
PcbBarcode = pcbBarcode,
MeasuredValue = "9.9999",
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(timeoutData);
// === 3단계: LEAK 시험 수행 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 3: LEAK 시험 대기 시작 (최대 30초)");
NotifyProgress(testIndex, "LEAK 시험중", "LightYellow");
// === MSSQL 데이터베이스 비동기 저장 (백프레셔 및 스레드 폭증 제어 적용) ===
try
{
dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath();
var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
var dbService = new EolDatabaseService(dbConnStr);
_ = Task.Run(async () =>
{
// 최대 10초 대기하며 세마포어 획득 (스레드 무한 누적 방지)
if (!await _dbSemaphore.WaitAsync(TimeSpan.FromSeconds(10)))
{
NotifyError(testIndex, "DB 저장 대기 초과 (네트워크 장애)");
return;
}
try
{
await dbService.SaveEolDataAsync(timeoutData, _loginInfo);
NotifyProgress(testIndex, "DB 저장 완료", "LightBlue");
}
catch (Exception ex)
{
NotifyError(testIndex, $"DB 저장 실패: [{ex.GetType().Name}] {ex.Message}");
}
finally
{
_dbSemaphore.Release();
}
});
}
catch (Exception ex)
{
Console.WriteLine("[DB Timeout Integration Error] " + ex.Message);
}
// C28에서 결과를 수신할 때까지 대기 (레거시와 동일하게 30초 설정)
bool received = resultReady.Wait(TimeSpan.FromSeconds(30));
continue;
}
if (!received)
{
Debug.WriteLine($"[TestProcess-{side}] ERROR: Sentinel C28 통신 타임아웃 발생!");
NotifyError(testIndex, "Sentinel C28 통신 타임아웃");
_dioBoard.WriteOutput(side + "_NG", true);
var leakResult = isLeft ? _leftResult : _rightResult;
if (leakResult == null)
{
NotifyError(testIndex, "LEAK 센서 데이터 오류");
_dioBoard.WriteOutput(side + "_NG", true);
continue;
}
// 타임아웃 시에도 UI(그리드)에 NG로 기록되도록 이벤트 발생
TestCompleted?.Invoke(this, new TestResultEventArgs
// === 4단계: 판정 ===
double specUL = ConfigManager.Current.SpecUL;
double specLL = ConfigManager.Current.SpecLL;
string programJudgment = SentinelParser.EvaluateJudgment(leakResult.MeasuredValue, specUL, specLL);
string sensorJudgment = leakResult.SensorJudgment ?? leakResult.Judgment ?? "-";
Debug.WriteLine($"[TestProcess-{side}] Phase 4: 프로그램 판정={programJudgment}, 센서자체판정={sensorJudgment}, 측정값={leakResult.MeasuredValue}");
// === 5단계: SPEC 교차 검증 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 5: SPEC 교차 검증 수행");
bool specMismatch = false;
string normalizedProgram = (programJudgment == "OK") ? "A" : "R";
string normalizedSensor = sensorJudgment;
// 센서 판정이 Accept/Reject, Pass/Fail, OK/NG 형식인 경우 모두 A/R로 정규화
if (sensorJudgment == "OK" || sensorJudgment == "A" || sensorJudgment == "P") normalizedSensor = "A";
else if (sensorJudgment == "NG" || sensorJudgment == "R" || sensorJudgment == "F") normalizedSensor = "R";
if (normalizedProgram != normalizedSensor && normalizedSensor != "-")
{
TestIndex = testIndex,
SensorData = sensorData,
MeasuredValue = "0.0000",
Judgment = "NG",
SensorJudgment = "T/O", // Timeout
SpecMismatch = false,
ProductionDate = DateTime.Now
});
specMismatch = true;
Console.WriteLine($"[TestProcess] {side} SPEC Mismatch! Program={programJudgment}, Sensor={sensorJudgment}");
}
// 로그 파일에도 기록
var timeoutData = new InspectData
// === 6단계: 로그 기록 ===
var inspectData = new InspectData
{
InspectDate = DateTime.Now.ToString("yyyy-MM-dd"),
InspectTime = DateTime.Now.ToString("HH:mm:ss"),
@ -385,10 +491,10 @@ namespace leak_test_project.Services
ProductId = sensorData.ID,
IcSn = sensorData.ID,
PcbBarcode = pcbBarcode,
MeasuredValue = "0.0000",
Judgment = "NG",
SpecUL = ConfigManager.Current.SpecUL.ToString("F2"),
SpecLL = ConfigManager.Current.SpecLL.ToString("F2"),
MeasuredValue = leakResult.MeasuredValueString,
Judgment = programJudgment,
SpecUL = specUL.ToString("F2"),
SpecLL = specLL.ToString("F2"),
Maker = _loginInfo?.Maker ?? string.Empty,
Model = _loginInfo?.Model ?? string.Empty,
Variant1 = _loginInfo?.Variant1 ?? string.Empty,
@ -399,9 +505,9 @@ namespace leak_test_project.Services
JigNo = _loginInfo?.JigNo ?? string.Empty,
ProductionDate = DateTime.Now
};
FileLogger.LogInspectData(timeoutData);
FileLogger.LogInspectData(inspectData);
// === MSSQL 데이터베이스 비동기 저장 ===
// === MSSQL 데이터베이스 비동기 저장 (백프레셔 및 스레드 폭증 제어 적용) ===
try
{
dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath();
@ -409,135 +515,72 @@ namespace leak_test_project.Services
var dbService = new EolDatabaseService(dbConnStr);
_ = Task.Run(async () =>
{
if (!await _dbSemaphore.WaitAsync(TimeSpan.FromSeconds(10)))
{
NotifyError(testIndex, "DB 저장 대기 초과 (네트워크 장애)");
return;
}
try
{
await dbService.SaveEolDataAsync(timeoutData, _loginInfo);
await dbService.SaveEolDataAsync(inspectData, _loginInfo);
NotifyProgress(testIndex, "DB 저장 완료", "LightBlue");
}
catch (Exception ex)
{
NotifyError(testIndex, $"DB 저장 실패: [{ex.GetType().Name}] {ex.Message}");
}
finally
{
_dbSemaphore.Release();
}
});
}
catch (Exception ex)
{
Console.WriteLine("[DB Timeout Integration Error] " + ex.Message);
Console.WriteLine("[DB Integration Error] " + ex.Message);
}
continue;
}
var leakResult = isLeft ? _leftResult : _rightResult;
if (leakResult == null)
{
NotifyError(testIndex, "LEAK 센서 데이터 오류");
_dioBoard.WriteOutput(side + "_NG", true);
continue;
}
// === 4단계: 판정 ===
double specUL = ConfigManager.Current.SpecUL;
double specLL = ConfigManager.Current.SpecLL;
string programJudgment = SentinelParser.EvaluateJudgment(leakResult.MeasuredValue, specUL, specLL);
string sensorJudgment = leakResult.SensorJudgment ?? leakResult.Judgment ?? "-";
Debug.WriteLine($"[TestProcess-{side}] Phase 4: 프로그램 판정={programJudgment}, 센서자체판정={sensorJudgment}, 측정값={leakResult.MeasuredValue}");
// === 5단계: SPEC 교차 검증 ===
Debug.WriteLine($"[TestProcess-{side}] Phase 5: SPEC 교차 검증 수행");
bool specMismatch = false;
string normalizedProgram = (programJudgment == "OK") ? "A" : "R";
string normalizedSensor = sensorJudgment;
// 센서 판정이 Accept/Reject, Pass/Fail, OK/NG 형식인 경우 모두 A/R로 정규화
if (sensorJudgment == "OK" || sensorJudgment == "A" || sensorJudgment == "P") normalizedSensor = "A";
else if (sensorJudgment == "NG" || sensorJudgment == "R" || sensorJudgment == "F") normalizedSensor = "R";
// === 7단계: DIO 출력 ===
if (programJudgment == "OK")
{
_dioBoard.WriteOutput(side + "_OK", true);
}
else
{
_dioBoard.WriteOutput(side + "_NG", true);
}
if (normalizedProgram != normalizedSensor && normalizedSensor != "-")
{
specMismatch = true;
Console.WriteLine($"[TestProcess] {side} SPEC Mismatch! Program={programJudgment}, Sensor={sensorJudgment}");
}
// 완료 알림
NotifyProgress(testIndex, "시험 완료", "LightBlue");
// === 6단계: 로그 기록 ===
var inspectData = new InspectData
{
InspectDate = DateTime.Now.ToString("yyyy-MM-dd"),
InspectTime = DateTime.Now.ToString("HH:mm:ss"),
Channel = side,
ProductId = sensorData.ID,
IcSn = sensorData.ID,
PcbBarcode = pcbBarcode,
MeasuredValue = leakResult.MeasuredValueString,
Judgment = programJudgment,
SpecUL = specUL.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
{
dbIniPath = Housing.Services.DatabaseSettings.GetDefaultFilePath();
var dbConnStr = Housing.Services.DatabaseSettings.Load(dbIniPath).ConnectionString;
var dbService = new EolDatabaseService(dbConnStr);
_ = Task.Run(async () =>
TestCompleted?.Invoke(this, new TestResultEventArgs
{
try
{
await dbService.SaveEolDataAsync(inspectData, _loginInfo);
NotifyProgress(testIndex, "DB 저장 완료", "LightBlue");
}
catch (Exception ex)
{
NotifyError(testIndex, $"DB 저장 실패: [{ex.GetType().Name}] {ex.Message}");
}
TestIndex = testIndex,
SensorData = sensorData,
PcbBarcode = pcbBarcode,
MeasuredValue = leakResult.MeasuredValueString,
Judgment = programJudgment,
SensorJudgment = sensorJudgment,
SpecMismatch = specMismatch,
ProductionDate = inspectData.ProductionDate ?? DateTime.Now
});
}
catch (Exception ex)
{
Console.WriteLine("[DB Integration Error] " + ex.Message);
}
// === 7단계: DIO 출력 ===
if (programJudgment == "OK")
finally
{
_dioBoard.WriteOutput(side + "_OK", true);
_activeTestIndex = -1;
_testLock.Release();
}
else
{
_dioBoard.WriteOutput(side + "_NG", true);
}
// 완료 알림
NotifyProgress(testIndex, "시험 완료", "LightBlue");
TestCompleted?.Invoke(this, new TestResultEventArgs
{
TestIndex = testIndex,
SensorData = sensorData,
PcbBarcode = pcbBarcode,
MeasuredValue = leakResult.MeasuredValueString,
Judgment = programJudgment,
SensorJudgment = sensorJudgment,
SpecMismatch = specMismatch,
ProductionDate = inspectData.ProductionDate ?? DateTime.Now
});
}
finally
catch (ThreadAbortException)
{
break; // 스레드 강제 종료 시 탈출
}
catch (Exception ex)
{
_activeTestIndex = -1;
_testLock.Release();
// [Critical] 루프 내부에서 예상치 못한 치명적 예외 발생 시 로깅 후 스레드 강제 복구(재시작)
FileLogger.Log("CRITICAL", $"[TestProcess-{side}] Thread Loop Exception: {ex.ToString()}");
NotifyError(testIndex, "시스템 치명적 오류 복구 중...");
Thread.Sleep(2000); // 연속 예외 방지 대기
}
}
}

309
leak_test_project/Utils/LogUtils.cs

@ -16,48 +16,94 @@ namespace leak_test_project.Utils
private static readonly string LogDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
private static readonly object _lock = new object();
// 24시간 가동 시 디스크 공간 모니터링 이벤트 및 변수
public static event EventHandler<string> DiskSpaceWarning;
private static DateTime _lastDiskCheckTime = DateTime.MinValue;
private const double DiskCheckIntervalMinutes = 5; // 5분마다 체크
private const long MinDiskSpaceBytes = 1024L * 1024L * 1024L; // 1GB
private static bool _isCheckingSpace = false;
private static void CheckDiskSpace()
{
if (_isCheckingSpace) return;
try
{
_isCheckingSpace = true;
if ((DateTime.Now - _lastDiskCheckTime).TotalMinutes < DiskCheckIntervalMinutes)
return;
_lastDiskCheckTime = DateTime.Now;
string rootDrive = Path.GetPathRoot(LogDirectory);
if (string.IsNullOrEmpty(rootDrive)) return;
var driveInfo = new DriveInfo(rootDrive);
if (driveInfo.IsReady && driveInfo.AvailableFreeSpace <= MinDiskSpaceBytes)
{
string warningMsg = $"[Disk Space Warning] 디스크 용량이 1GB 이하입니다. 현재 여유 공간: {(double)driveInfo.AvailableFreeSpace / (1024 * 1024):F2} MB. 디스크 공간을 정리하십시오.";
DiskSpaceWarning?.Invoke(null, warningMsg);
// 디스크 공간 부족 시 로그 파일 쓰기 중단이 아니라 에러 이벤트 표출만 수행하므로 로깅도 남김
Log("WARNING", warningMsg);
}
}
catch (Exception ex)
{
Console.WriteLine($"[Disk Space Check Error] {ex.Message}");
}
finally
{
_isCheckingSpace = false;
}
}
/// <summary>
/// 검사 데이터를 Logs/yyyy-MM-dd.csv 파일에 저장함 (16컬럼)
/// </summary>
public static void LogInspectData(InspectData data)
{
lock (_lock)
// 검사 제어 스레드 블로킹 해제를 위해 Task.Run으로 비동기 실행
Task.Run(() =>
{
try
lock (_lock)
{
if (!Directory.Exists(LogDirectory))
Directory.CreateDirectory(LogDirectory);
try
{
CheckDiskSpace();
string dateStr = DateTime.Now.ToString("yyyy-MM-dd");
string filePath = Path.Combine(LogDirectory, $"{dateStr}.csv");
if (!Directory.Exists(LogDirectory))
Directory.CreateDirectory(LogDirectory);
bool isNewFile = !File.Exists(filePath);
string dateStr = DateTime.Now.ToString("yyyy-MM-dd");
string filePath = Path.Combine(LogDirectory, $"{dateStr}.csv");
// 신규 CSV 헤더: DB 스키마 16개 컬럼과 일치
if (isNewFile)
{
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);
}
bool isNewFile = !File.Exists(filePath);
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");
}
// 신규 CSV 헤더: DB 스키마 16개 컬럼과 일치
if (isNewFile)
{
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.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}";
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");
}
File.AppendAllText(filePath, csvLine, Encoding.UTF8);
}
catch (Exception ex)
{
Console.WriteLine($"[FileLogger Error] {ex.Message}");
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);
}
catch (Exception ex)
{
Console.WriteLine($"[FileLogger Error] {ex.Message}");
}
}
}
});
}
/// <summary>
@ -65,24 +111,30 @@ namespace leak_test_project.Utils
/// </summary>
public static void Log(string tag, string message)
{
lock (_lock)
// 비동기로 디스크 쓰기 수행
Task.Run(() =>
{
try
lock (_lock)
{
if (!Directory.Exists(LogDirectory))
Directory.CreateDirectory(LogDirectory);
try
{
CheckDiskSpace();
string dateStr = DateTime.Now.ToString("yyyy-MM-dd");
string filePath = Path.Combine(LogDirectory, $"{dateStr}_system.log");
if (!Directory.Exists(LogDirectory))
Directory.CreateDirectory(LogDirectory);
string logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] [{tag}] {message}{Environment.NewLine}";
File.AppendAllText(filePath, logEntry, Encoding.UTF8);
}
catch (Exception ex)
{
Console.WriteLine($"[FileLogger Error] {ex.Message}");
string dateStr = DateTime.Now.ToString("yyyy-MM-dd");
string filePath = Path.Combine(LogDirectory, $"{dateStr}_system.log");
string logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] [{tag}] {message}{Environment.NewLine}";
File.AppendAllText(filePath, logEntry, Encoding.UTF8);
}
catch (Exception ex)
{
Console.WriteLine($"[FileLogger Error] {ex.Message}");
}
}
}
});
}
/// <summary>
/// CSV 값 이스케이프: 쉼표, 따옴표, 줄바꿈이 포함된 값을 안전하게 감싸줌
@ -155,128 +207,71 @@ namespace leak_test_project.Utils
string line = lines[i];
if (string.IsNullOrWhiteSpace(line)) continue;
var parts = line.Split(',');
if (parts.Length < 8) continue;
InspectData data = null;
if (parts.Length >= 16)
if (parts.Length < 16) continue;
// 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))
{
// 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") ?? ""
};
prodDate = dt;
}
else
else if (DateTime.TryParse(prodDateRaw, out DateTime dtFallback))
{
// 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
};
prodDate = dtFallback;
}
if (data != null)
// 필터링 적용 (부분 일치 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;
InspectData data = new InspectData
{
results.Add(data);
}
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") ?? ""
};
results.Add(data);
}
}
catch (Exception ex)

1
leak_test_project/ViewModels/DataViewModel.cs

@ -128,6 +128,7 @@ namespace leak_test_project.ViewModels
catch (OperationCanceledException)
{
_dialogService.ShowMessage("조회가 취소되었습니다.");
CloseCommand.Execute(null);
}
catch (Exception ex)
{

15
leak_test_project/ViewModels/HomeViewModel.cs

@ -107,6 +107,15 @@ namespace leak_test_project.ViewModels
InitializeTestProcess(config);
ConfigManager.ConfigChanged += OnConfigChanged;
FileLogger.DiskSpaceWarning += OnDiskSpaceWarning;
}
private void OnDiskSpaceWarning(object sender, string message)
{
_dispatcher.Invoke(() => {
LeftError = message;
RightError = message;
});
}
private void OnConfigChanged(object sender, EventArgs e)
@ -322,7 +331,10 @@ namespace leak_test_project.ViewModels
if (e.TestIndex == 0) LeftError = msg;
else RightError = msg;
_dialogService.ShowWarning("프로그램 스팩과 센서의 스팩이 서로 맞지 않습니다.", "SPEC 교차 검증 경고");
if (ConfigManager.Current.EnableSpecMismatchAlarm)
{
_dialogService.ShowWarning("프로그램 스팩과 센서의 스팩이 서로 맞지 않습니다.", "SPEC 교차 검증 경고");
}
}
});
@ -414,6 +426,7 @@ namespace leak_test_project.ViewModels
public void Dispose()
{
ConfigManager.ConfigChanged -= OnConfigChanged;
FileLogger.DiskSpaceWarning -= OnDiskSpaceWarning;
CleanupAll();
}
}

14
leak_test_project/Views/CommunicationWindow.xaml

@ -62,8 +62,13 @@
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- LEFT COLUMN: 4251 BOARD SETTINGS -->
<StackPanel Grid.Column="0" Margin="10,0">
<StackPanel Grid.Row="0" Grid.Column="0" Margin="10,0">
<TextBlock Text="[ 4251 보드 설정 ]" FontSize="16" FontWeight="Bold" Foreground="#2E86C1" Margin="0,0,0,15"/>
<!-- Board 4251 BaudRate -->
@ -93,7 +98,7 @@
</StackPanel>
<!-- RIGHT COLUMN: LEAK SENSOR (SENTINEL) SETTINGS -->
<StackPanel Grid.Column="1" Margin="10,0">
<StackPanel Grid.Row="0" Grid.Column="1" Margin="10,0">
<TextBlock Text="[ Sentinel C28 설정 ]" FontSize="16" FontWeight="Bold" Foreground="#2E86C1" Margin="0,0,0,15"/>
<!-- Sensor BaudRate -->
@ -121,6 +126,11 @@
</Border>
</StackPanel>
</StackPanel>
<!-- BOTTOM CHECKBOX FOR SPEC MISMATCH ALARM -->
<StackPanel Grid.Row="1" Grid.ColumnSpan="2" HorizontalAlignment="Center" Margin="10,25,10,5">
<CheckBox x:Name="chkEnableSpecMismatchAlarm" Content="스펙 불일치 알림 활성화" FontSize="15" FontWeight="Bold" Foreground="#2C3E50" VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</ScrollViewer>

6
leak_test_project/Views/CommunicationWindow.xaml.cs

@ -45,6 +45,9 @@ namespace leak_test_project.Views
break;
}
}
// 스펙 불일치 알림 체크박스 상태 로드
chkEnableSpecMismatchAlarm.IsChecked = config.EnableSpecMismatchAlarm;
}
private void BtnSave_Click(object sender, RoutedEventArgs e)
@ -68,6 +71,9 @@ namespace leak_test_project.Views
config.Board4251BaudRate = baud;
}
// 스펙 불일치 알림 설정값 저장
config.EnableSpecMismatchAlarm = chkEnableSpecMismatchAlarm.IsChecked ?? false;
ConfigManager.Save();
MessageBox.Show("통신 설정이 저장되었습니다.");
this.DialogResult = true;

3
leak_test_project/Views/StartupLoginWindow.xaml

@ -15,7 +15,8 @@
WindowStartupLocation="CenterScreen"
Background="{StaticResource HmiScreenBackgroundBrush}"
UseLayoutRounding="True"
SnapsToDevicePixels="True">
SnapsToDevicePixels="True"
Icon="/EOL.ico">
<Window.Resources>
<Style x:Key="FieldLabelStyle" TargetType="TextBlock">

130
leak_test_project/leak_test_project.csproj

@ -19,6 +19,7 @@
<!-- 네이티브 DLL들도 단일 EXE 안에 내장하여 추출 방식으로 처리 -->
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<ApplicationIcon>EOL.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
@ -29,6 +30,10 @@
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>
<ItemGroup>
<Resource Include="EOL.ico" />
</ItemGroup>
<ItemGroup>
<None Update="Database.ini">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@ -42,6 +47,9 @@
<None Update="LoginOptions.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="EOL.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<!-- 빌드 후 Native DLL을 libs 하위 폴더로 복사 (XCopy 플랫폼 의존성 배제) -->
@ -53,8 +61,15 @@
<Copy SourceFiles="@(NativeDlls)" DestinationFolder="$(TargetDir)libs\" SkipUnchangedFiles="true" />
</Target>
<!-- Publish 단계 완료 후 Native DLL을 libs 하위 폴더로 복사 -->
<Target Name="CopyDllToPublish" AfterTargets="Publish">
<!-- Visual Studio IDE 게시 엔진이 파편 파일들(exe, pdb, libs)을 게시 폴더로 복사하는 것을 방지 -->
<Target Name="FilterFilesToPublish" AfterTargets="ComputeFilesToPublish">
<ItemGroup>
<ResolvedFileToPublish Remove="@(ResolvedFileToPublish)" />
</ItemGroup>
</Target>
<!-- Publish 단계 완료 후 Native DLL을 libs 하위 폴더로 복사 (FolderPublish 실행 전에 수행) -->
<Target Name="CopyDllToPublish" BeforeTargets="FolderPublish" AfterTargets="Publish">
<ItemGroup>
<PublishNativeDlls Include="$(MSBuildProjectDirectory)\PCI-Dask.dll" />
</ItemGroup>
@ -62,83 +77,68 @@
<Copy SourceFiles="@(PublishNativeDlls)" DestinationFolder="$(PublishDir)libs\" SkipUnchangedFiles="true" />
</Target>
<!-- C:\LeakTest\ 설치 전용 Target (Publish에 의존하며, PublishDir로부터 복사 실행) -->
<Target Name="FolderPublish" DependsOnTargets="Publish" AfterTargets="Publish">
<!-- C:\LeakTest\ 단일 설치 패키지 패키징 Target (게시 폴더 C:\Users\COMPUTER1\Downloads\ 에 오직 LeakTestSetup.exe 1개만 보존) -->
<Target Name="FolderPublish" DependsOnTargets="Publish" AfterTargets="Publish;AfterPublish;FileSystemPublish">
<PropertyGroup>
<DeployTargetFolder>C:\LeakTest\</DeployTargetFolder>
<TempDeployFolder>$(MSBuildProjectDirectory)\bin\Release\net9.0-windows\win-x86\temp_deploy\</TempDeployFolder>
<PublishBackupDir>$(MSBuildProjectDirectory)\bin\Release\net9.0-windows\win-x86\publish_backup\</PublishBackupDir>
<SafePublishDir>$(MSBuildProjectDirectory)\bin\Release\net9.0-windows\win-x86\publish\</SafePublishDir>
</PropertyGroup>
<Message Text="Deploying clean publish output to $(DeployTargetFolder)..." Importance="high" />
<!-- 기존 대상 디렉토리 클린업 및 재생성 (지저분한 기존 dll 파일들 소거) -->
<RemoveDir Directories="$(DeployTargetFolder)" />
<MakeDir Directories="$(DeployTargetFolder)" />
<MakeDir Directories="$(DeployTargetFolder)Logs" />
<!-- 실행 파일, 런타임 구성 파일 복사 -->
<Copy SourceFiles="$(PublishDir)leak_test_project.exe" DestinationFolder="$(DeployTargetFolder)" />
<Copy SourceFiles="$(TargetDir)leak_test_project.dll.config" DestinationFolder="$(DeployTargetFolder)" />
<!-- 설정 파일 복사 (프로젝트 소스에서 직접 이관) -->
<Copy SourceFiles="$(MSBuildProjectDirectory)\Database.ini" DestinationFolder="$(DeployTargetFolder)" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\LoginOptions.json" DestinationFolder="$(DeployTargetFolder)" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Sensor.ini" DestinationFolder="$(DeployTargetFolder)" Condition="Exists('$(MSBuildProjectDirectory)\Sensor.ini')" />
<!-- Native DLLs 복사: [DllImport("PCI-Dask.dll")] 검색 경로는 EXE 실행 폴더(루트)이므로 루트에 배치 -->
<Copy SourceFiles="$(MSBuildProjectDirectory)\PCI-Dask.dll" DestinationFolder="$(DeployTargetFolder)" />
<Copy SourceFiles="$(TargetDir)Microsoft.Data.SqlClient.SNI.dll" DestinationFolder="$(DeployTargetFolder)" />
<!-- [WiX Toolset 단일 MSI 설치 파일 자동 빌드 스텝] -->
<Message Text="Preparing isolated packaging environment at $(TempDeployFolder)..." Importance="high" />
<!-- 1. 임시 수집 폴더 클린업 및 생성 -->
<RemoveDir Directories="$(TempDeployFolder)" />
<MakeDir Directories="$(TempDeployFolder)" />
<!-- 2. 게시 바이너리 및 필수 설정 파일 수집 이관 -->
<Copy SourceFiles="$(PublishDir)leak_test_project.exe" DestinationFolder="$(TempDeployFolder)" Condition="Exists('$(PublishDir)leak_test_project.exe')" />
<Copy SourceFiles="$(TargetDir)leak_test_project.exe" DestinationFolder="$(TempDeployFolder)" Condition="!Exists('$(PublishDir)leak_test_project.exe') And Exists('$(TargetDir)leak_test_project.exe')" />
<Copy SourceFiles="$(TargetDir)leak_test_project.dll.config" DestinationFolder="$(TempDeployFolder)" Condition="Exists('$(TargetDir)leak_test_project.dll.config')" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Database.ini" DestinationFolder="$(TempDeployFolder)" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\LoginOptions.json" DestinationFolder="$(TempDeployFolder)" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\EOL.ico" DestinationFolder="$(TempDeployFolder)" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\Sensor.ini" DestinationFolder="$(TempDeployFolder)" Condition="Exists('$(MSBuildProjectDirectory)\Sensor.ini')" />
<Copy SourceFiles="$(MSBuildProjectDirectory)\PCI-Dask.dll" DestinationFolder="$(TempDeployFolder)" />
<Copy SourceFiles="$(TargetDir)Microsoft.Data.SqlClient.SNI.dll" DestinationFolder="$(TempDeployFolder)" />
<!-- 3. [WiX Toolset 단일 MSI 설치 파일 자동 빌드] -->
<Message Text="Compiling single installer MSI via WiX Toolset..." Importance="high" />
<Exec Command="wix build &quot;$(MSBuildProjectDirectory)\..\leak_test_project.Setup\Product.wxs&quot; -o &quot;$(DeployTargetFolder)LeakTestSetup.msi&quot;" WorkingDirectory="$(MSBuildProjectDirectory)" />
<Exec Command="wix build &quot;$(MSBuildProjectDirectory)\..\leak_test_project.Setup\Product.wxs&quot; -d PublishSourceDir=&quot;$(TempDeployFolder).&quot; -o &quot;$(TempDeployFolder)LeakTestSetup.msi&quot;" WorkingDirectory="$(MSBuildProjectDirectory)" />
<!-- [WiX Toolset 단일 EXE 설치 파일 자동 빌드 스텝] -->
<!-- 4. [WiX Toolset 단일 EXE 설치 파일 자동 빌드] -->
<Message Text="Compiling single installer EXE via WiX Toolset Bundle..." Importance="high" />
<Exec Command="wix build &quot;$(MSBuildProjectDirectory)\..\leak_test_project.Setup\Bundle.wxs&quot; -o &quot;$(DeployTargetFolder)LeakTestSetup.exe&quot; -ext WixToolset.BootstrapperApplications.wixext/5.0.2" WorkingDirectory="$(MSBuildProjectDirectory)" />
<Exec Command="wix build &quot;$(MSBuildProjectDirectory)\..\leak_test_project.Setup\Bundle.wxs&quot; -d PublishSourceDir=&quot;$(TempDeployFolder).&quot; -d MsiPath=&quot;$(TempDeployFolder)LeakTestSetup.msi&quot; -o &quot;$(TempDeployFolder)LeakTestSetup.exe&quot; -ext WixToolset.BootstrapperApplications.wixext/5.0.2" WorkingDirectory="$(MSBuildProjectDirectory)" />
<!-- 빌드 중간 산출물 .wixpdb 및 MSI 파일 정리 (C:\LeakTest 폴더 깔끔화) -->
<Delete Files="$(DeployTargetFolder)LeakTestSetup.wixpdb" />
<Delete Files="$(DeployTargetFolder)LeakTestSetup.msi" />
<!-- 비주얼 스튜디오 게시 출력 폴더(PublishDir) 정화 작업 -->
<Message Text="Purging publish output directory and leaving only LeakTestSetup.exe..." Importance="high" />
<!-- 1. LeakTestSetup.exe 파일을 임시 대피 경로로 복사 -->
<!-- 5. 완성된 LeakTestSetup.exe 단일 파일 대피 -->
<RemoveDir Directories="$(PublishBackupDir)" />
<MakeDir Directories="$(PublishBackupDir)" />
<Copy SourceFiles="$(DeployTargetFolder)LeakTestSetup.exe" DestinationFolder="$(PublishBackupDir)" />
<Copy SourceFiles="$(TempDeployFolder)LeakTestSetup.exe" DestinationFolder="$(PublishBackupDir)" />
<!-- 6. 임시 수집 디렉터리 완전 소거 -->
<RemoveDir Directories="$(TempDeployFolder)" />
<!-- 2. 게시 폴더(PublishDir) 하위의 모든 자원 삭제 (dll, config, json, exe, libs 폴더 등) -->
<!-- 7. 게시 폴더(PublishDir 및 SafePublishDir) 하위의 프로젝트 관련 파편 파일 및 libs 폴더 정밀 완전 삭제 -->
<Message Text="Purging publish output directory and leaving only LeakTestSetup.exe..." Importance="high" />
<ItemGroup>
<FilesToClean Include="$(PublishDir)*.dll" />
<FilesToClean Include="$(PublishDir)*.exe" />
<FilesToClean Include="$(PublishDir)*.config" />
<FilesToClean Include="$(PublishDir)*.json" />
<FilesToClean Include="$(PublishDir)*.pdb" />
<FilesToClean Include="$(PublishDir)*.ini" />
<!-- SafePublishDir 정화 -->
<FilesToPurge Include="$(SafePublishDir)leak_test_project.exe;$(SafePublishDir)leak_test_project.pdb;$(SafePublishDir)leak_test_project.dll;$(SafePublishDir)leak_test_project.dll.config;$(SafePublishDir)PCI-Dask.dll;$(SafePublishDir)Microsoft.Data.SqlClient.SNI.dll;$(SafePublishDir)Database.ini;$(SafePublishDir)LoginOptions.json;$(SafePublishDir)EOL.ico;$(SafePublishDir)Sensor.ini;$(SafePublishDir)LeakTestSetup.msi;$(SafePublishDir)LeakTestSetup.wixpdb" />
<!-- PublishDir 정화 (C:\Users\COMPUTER1\Downloads\ 게시 폴더) -->
<FilesToPurge Include="$(PublishDir)leak_test_project.exe;$(PublishDir)leak_test_project.pdb;$(PublishDir)leak_test_project.dll;$(PublishDir)leak_test_project.dll.config;$(PublishDir)PCI-Dask.dll;$(PublishDir)Microsoft.Data.SqlClient.SNI.dll;$(PublishDir)Database.ini;$(PublishDir)LoginOptions.json;$(PublishDir)EOL.ico;$(PublishDir)Sensor.ini;$(PublishDir)LeakTestSetup.msi;$(PublishDir)LeakTestSetup.wixpdb" Condition="'$(PublishDir)' != ''" />
</ItemGroup>
<Delete Files="@(FilesToClean)" />
<RemoveDir Directories="$(PublishDir)libs" />
<RemoveDir Directories="$(PublishDir)cs" />
<RemoveDir Directories="$(PublishDir)de" />
<RemoveDir Directories="$(PublishDir)es" />
<RemoveDir Directories="$(PublishDir)fr" />
<RemoveDir Directories="$(PublishDir)it" />
<RemoveDir Directories="$(PublishDir)ja" />
<RemoveDir Directories="$(PublishDir)ko" />
<RemoveDir Directories="$(PublishDir)pl" />
<RemoveDir Directories="$(PublishDir)pt-BR" />
<RemoveDir Directories="$(PublishDir)ru" />
<RemoveDir Directories="$(PublishDir)tr" />
<RemoveDir Directories="$(PublishDir)zh-Hans" />
<RemoveDir Directories="$(PublishDir)zh-Hant" />
<!-- 3. 대피시켰던 단일 설치 EXE 파일만 다시 게시 폴더(PublishDir)로 원복 이관 -->
<Copy SourceFiles="$(PublishBackupDir)LeakTestSetup.exe" DestinationFolder="$(PublishDir)" />
<!-- 4. 대피 임시 폴더 삭제 정리 -->
<Delete Files="@(FilesToPurge)" />
<!-- libs 및 부속 언어 폴더 완전 삭제 -->
<RemoveDir Directories="$(SafePublishDir)libs;$(SafePublishDir)cs;$(SafePublishDir)de;$(SafePublishDir)es;$(SafePublishDir)fr;$(SafePublishDir)it;$(SafePublishDir)ja;$(SafePublishDir)ko;$(SafePublishDir)pl;$(SafePublishDir)pt-BR;$(SafePublishDir)ru;$(SafePublishDir)tr;$(SafePublishDir)zh-Hans;$(SafePublishDir)zh-Hant" />
<RemoveDir Directories="$(PublishDir)libs;$(PublishDir)cs;$(PublishDir)de;$(PublishDir)es;$(PublishDir)fr;$(PublishDir)it;$(PublishDir)ja;$(PublishDir)ko;$(PublishDir)pl;$(PublishDir)pt-BR;$(PublishDir)ru;$(PublishDir)tr;$(PublishDir)zh-Hans;$(PublishDir)zh-Hant" Condition="'$(PublishDir)' != ''" />
<!-- 8. 대피시켰던 단일 설치 EXE 파일 1개만 게시 폴더로 이관 -->
<Copy SourceFiles="$(PublishBackupDir)LeakTestSetup.exe" DestinationFolder="$(SafePublishDir)" />
<Copy SourceFiles="$(PublishBackupDir)LeakTestSetup.exe" DestinationFolder="$(PublishDir)" Condition="'$(PublishDir)' != '' And '$(PublishDir)' != '$(SafePublishDir)'" />
<!-- 9. 대피 임시 폴더 정리 -->
<RemoveDir Directories="$(PublishBackupDir)" />
<Message Text="Single Installer successfully compiled at both C:\LeakTest\LeakTestSetup.exe and $(PublishDir)LeakTestSetup.exe" Importance="high" />
<Message Text="Single Installer LeakTestSetup.exe successfully generated at $(PublishDir)LeakTestSetup.exe" Importance="high" />
</Target>
</Project>

60
ob/05_Implementation_History.md

@ -163,4 +163,64 @@
3. **Line 컬럼 잘림 오류 수정**: DB의 [dbo].[EOL] 테이블 스키마 파일([dbo.EOL_Schema.sql](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/DB/dbo.EOL_Schema.sql))에서 작업 라인을 나타내는 `Line` 컬럼의 물리 데이터 형식이 `NVARCHAR(5)`로 좁게 선언되어 있어 5자를 초과하는 라인 정보(예: `"LINE-1"`은 6자) 입력 시 잘림 오류(Truncation Exception)로 인해 저장이 실패하던 버그를 확인하고, 테이블 DDL 스키마 및 문서 상의 `Line` 규격을 `NVARCHAR(30)`로 변경 및 확장 조치하였습니다.
4. **Line 컬럼 가공 처리 원복**: 라인 정보에 대해 숫자만 남기도록 하던 임시 정규식 필터링 코드를 제거하고, 원래 문자열 형태(예: `"LINE-1"`) 그대로 데이터베이스 및 CSV 로그에 기록되도록 소스코드를 복구하였습니다.
### 📅 2026-07-03 (24시간 무중단 가동 신뢰성 정밀 진단 및 코드 리팩토링)
* **내용**: 24시간 365일 실시간 검사 라인의 무중단 가동 신뢰성을 위한 SRE 관점의 소스 코드 리팩토링을 완료했습니다.
* **기능**:
1. **Sync-over-Async 제거**: `TestProcessService.cs` 내의 `.GetAwaiter().GetResult()` 호출부를 `Task.Run` 스레드 풀 분리를 통해 격리하여 컨텍스트 데드락 요인을 원천 방지했습니다.
2. **글로벌 스레드 복구**: `TestProcessService.cs``ProcessProc` 검사 루프 전체를 `try-catch` 구조로 격리하여 예외 시 스레드가 소멸하지 않고 자동 자가 치유 대기로 복구되도록 설계했습니다.
3. **TCS 동시성 및 lock 도입**: `Board4251.cs` 내부의 `_responseTcs` 할당/해제 시 `_tcsLock` 동기화 락을 사용하여 기기 수신과 송신 간의 레이스 컨디션을 완전히 방어했습니다.
4. **시리얼 버퍼 OOM 차단**: `SentinelC28Service.cs``_receiveBuffer``StringBuilder` 기반의 고정 크기 제한(64KB) 관리 구조로 전환하고, `Dispose()` 내에 기기 이벤트 해제를 강제하여 메모리 누수를 제거했습니다.
5. **1GB 디스크 경고 및 소프트 경고 구현**: `LogUtils.cs` 내에 디스크 공간 감지(1GB 이하) 메커니즘을 연동하되, 공정이 멈추거나 DB 저장이 누락되지 않도록 Soft Warning 형태로 에러 이벤트를 전파하고 화면과 로그에 경고만 노출하도록 처리했습니다.
6. **DB 백프레셔 제어**: 데이터 적재 시 `_dbSemaphore`를 통해 최대 동시 쓰기 스레드를 3개로 제어하여 네트워크 지연 시 스레드 풀 기아 상태가 발생하지 않도록 억제했습니다.
7. **빌드 무결성 보장**: 리팩토링 결과 `net9.0-windows` 컴파일 환경에서 0 warnings, 0 errors로 빌드를 정상 무결 완료했습니다.
8. **스펙 불일치 알림 스위치 추가**: 통신 설정 메뉴 하단에 스펙 불일치 알림 여부를 켜고 끌 수 있는 체크박스(`CheckBox` 기반 스위치)를 추가했습니다. 해당 스위치는 기본값이 **켜짐(ON)** 상태이며, 사용자의 선택값(EnableSpecMismatchAlarm)을 `config.xml`에 영속 저장하여 교차 판정 경고창 노출 여부를 동적으로 제어하도록 구현했습니다.
### 📅 2026-07-08 (메뉴얼 정합성 검토 및 최신 기능 현행화)
* **내용**: WPF 소스코드의 최신 UI 변경사항 및 백엔드 로직에 맞춰 사용자 메뉴얼 문서(10종)와의 정합성을 확보하고 병합 문서를 갱신했습니다.
* **기능**:
1. **설정 파일 공통 경로 통일**: 메뉴얼 상에서 혼재되어 있던 데이터 경로 규격을 실제 소스코드와 배포 규격에 대응하는 **`C:\LeakTest\`**로 전면 일치시켰습니다.
2. **시작/종료 및 식별자 2행 분리 UI 반영**: 메인 화면(`03_메인_화면.md`) 메뉴얼에 `시작/종료` 시간 변경 및 가로 잘림 튜닝이 반영된 `IC_SN` / `PCB_Barcode` 2행 분리 레이아웃 명세를 현행화했습니다.
3. **스펙 불일치 체크박스 및 경고 연동**: 통신 설정(`04_통신_설정.md`)과 오류 대응(`09_오류_대응.md`)에 `EnableSpecMismatchAlarm` 체크박스 옵션을 서술하고, 해제(OFF) 시의 경고 대화상자 노출 차단 동작을 보강했습니다.
4. **1.5단계 바코드 DB 매핑 및 저장 제외 흐름 반영**: 자동 시험 프로세스(`07_시험_프로세스.md`) 흐름도에 `dbo.Housing_Assembly` 기반 바코드 쿼리 단계를 신설하고, ID 판독 실패 및 바코드 매핑 실패 시 DB 및 CSV 로그 저장을 스킵하는 무결성 처리 과정을 서술했습니다.
5. **DB 스키마 사양 현행화**: `08_로그_관리.md` DDL 변경 내용에 맞춰 `Line` 크기 확장(`NVARCHAR(30)`) 및 개별 DB 연결 주소 파싱, SSL 오류 방지(`TrustServerCertificate=True`)에 대해 설명을 추가했습니다.
6. **통합 매뉴얼 빌드**: 병합 스크립트(`build_pdf.py`) 실행을 통해 `Manual_Merged.md``Manual_Merged.html` 결과물을 최신 사양으로 성공적으로 컴파일 완료했습니다.
### 📅 2026-07-10 (데이터 조회 취소 시 메인 화면 복귀 정합성 불일치 수정, 매뉴얼 톤앤매너 고도화, 구형 8컬럼 호환성 소거 및 개발자 매뉴얼 분리 이관)
* **내용**: 측정 데이터 조회 화면의 취소 복귀 오류를 수정하고 매뉴얼의 구어체 표현을 정비했으며, 구형 로그 호환성 삭제와 함께 사용자 매뉴얼에서 상세 기술 데이터들을 개발 지식베이스인 `ob/` 폴더 하위로 완전히 분리 이관했습니다.
* **기능**:
1. **조회 취소 시 화면 강제 복귀 연동**: [DataViewModel.cs](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/ViewModels/DataViewModel.cs)의 `ExecuteSearch` 비동기 태스크에서 취소 예외(`OperationCanceledException`) 발생 시, 메시지 박스가 표시된 직후 자동으로 `CloseCommand.Execute(null)`를 실행하여 사용자를 메인(홈) 화면으로 즉시 안전하게 돌려보내는 시퀀스를 보강하였습니다.
2. **매뉴얼 어투 격식화 및 병합 컴파일**: `06_데이터_조회.md` 내의 부적절한 구어체 표현(예: "콕 집어" 등) 및 과장된 톤을 명사형/서술형 설명서 어조로 다듬었으며, `build_pdf.py` 스크립트를 재구동하여 통합 매뉴얼(`Manual_Merged.md`, `Manual_Merged.html`)까지 정상 컴파일 완료하였습니다.
3. **구형 8개 컬럼 로그 호환성 로직 제거**: [LogUtils.cs](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Utils/LogUtils.cs)의 CSV 파서 엔진(`ParseLogsFromCsv`)에서 기존 8개 컬럼 포맷 분기 처리 로직을 완전히 삭제하고, 오직 EOL 신규 16개 표준 컬럼 규격만 정상 파싱하도록 단순화했습니다. 아울러 매뉴얼 문서(`06_데이터_조회.md`, `08_로그_관리.md`) 내 구형 포맷 관련 참고 사항 및 하위 호환성 설명을 삭제하였습니다.
4. **개발자 매뉴얼 분리 이관 및 사용자 매뉴얼 용어 순화**: 일반 사용자가 이해하기 어려운 C# 바인딩 프로퍼티명, 데이터베이스 기본키/외래키 제약조건(`PK/FK`, `UK`), 세마포어 잠금 등의 설계 지향적 기술 정보를 [ob/06_Developer_Manual.md](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/ob/06_Developer_Manual.md) 신규 문서로 통합 이전하여 보존하였습니다. 동시에 사용자 매뉴얼(`Manual/` 하위 파일)에 있던 관련 표현들은 모두 "순차 처리 잠금", "표 스타일" 등 쉬운 한국어 사용자 관점의 설명으로 순화하였습니다.
5. **빌드 안정성 확인**: 변경 사항 반영 후 `.NET 9.0 (WPF)` 환경에서 0 warnings, 0 errors로 빌드가 완벽히 동작하는지 검증을 완료하였습니다.
6. **타임아웃 시 누설값 기록 변경**: C28 센서 통신 타임아웃 발생 시 데이터베이스 및 CSV 로그에 강제 기입되던 누설 측정값 `"0.0000"`을 에러 상태의 시각적 명확화를 위해 데이터베이스 규격(`DECIMAL(5,4)`) 내 물리적 최댓값인 `"9.9999"`로 기록되도록 [TestProcessService.cs](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Services/TestProcessService.cs)를 수정하고, 관련 매뉴얼 문서([07_시험_프로세스.md](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Manual/07_시험_프로세스.md)) 설명을 갱신하였습니다.
7. **매뉴얼 내 프로그램 실행 경로 설명 수정**: 프로그램 실행 지침을 파일 탐색기에서 직접 exe 파일을 더블 클릭하여 실행하도록 설명된 방식에서, 바탕화면에 생성된 **바로가기 아이콘**을 사용하여 실행하도록 사용자 매뉴얼([02_초기_설정.md](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Manual/02_초기_설정.md), [사용자_메뉴얼.md](file:///c:/Users/COMPUTER1/Desktop/mobi/leak_test_project/leak_test_project/Manual/사용자_메뉴얼.md))의 설명을 갱신 및 컴파일 하였습니다.
### 📅 2026-07-21 (언인스톨 시 디렉토리 깔끔하게 삭제하도록 개선)
* **내용**: 프로그램 언인스톨(Uninstall) 시 런타임에 동적으로 생성된 설정 파일 및 설치 디렉토리를 완전히 청소하도록 배포 패키지 설정을 개선하였습니다. (단, 로그 파일은 사용자의 요청에 따라 안전하게 보존하도록 조치함)
* **기능**:
1. **CommonAppDataFolder 표준 디렉토리 연동**: `C:\ProgramData\LeakTest\` 디렉토리를 특정하여 관리하기 위해 WiX 스크립트 상에 `CommonAppDataFolder` 참조를 선언했습니다.
2. **언인스톨 시 C:\ProgramData\LeakTest 디렉토리 및 파일 완전 제거**: 언인스톨이 진행될 때 로그인 옵션(`LoginOptions.json`), 로그인 히스토리(`LoginHistory.json`) 등의 설정 파일들과 폴더 자체를 디스크에서 완전히 청소하도록 처리했습니다.
3. **로그 파일 보존 및 C:\LeakTest 루트 파일 제거**: 사용자의 요청에 따라 `C:\LeakTest\Logs\` 폴더 및 내부의 누수 검사 로그 파일(`*.csv`)들은 삭제 대상에서 철저히 배제하여 보존되도록 구현하였습니다. 동시에 `C:\LeakTest` 루트 경로 상의 실행 파일 및 설정/DLL 잔여 파일들은 와일드카드(`*`) 기반 `RemoveFile`을 사용해 말끔하게 삭제되도록 패키징 구성을 조정했습니다.
### 📅 2026-07-24 (실행 파일 및 배포 패키지 아이콘 적용)
* **내용**: 카카오톡으로 전달받은 `EOL.ico` 아이콘 파일을 프로젝트에 연동하여 실행 파일 아이콘, 각 윈도우 창(로그인 및 메인 화면), 제어판 및 바탕화면 바로가기 아이콘으로 일괄 적용하였습니다.
* **기능**:
1. **아이콘 복사 및 빌드 설정**: `EOL.ico`를 프로젝트 루트에 복사하고 `.csproj``<ApplicationIcon>EOL.ico</ApplicationIcon>` 및 빌드 이관/리소스 설정을 반영하여 빌드 컴파일 시 실행 파일(.exe)에 자동 패킹되도록 하였습니다.
2. **WPF 윈도우 창 아이콘 지정**: `MainWindow.xaml``StartupLoginWindow.xaml``Icon` 속성으로 `EOL.ico` 리소스를 연결하여 프로그램 실행 시 창 타이틀바와 작업 표시줄에 아이콘이 일관성 있게 표시되도록 구현했습니다.
3. **설치 패키지(WiX) 아이콘 연동**: `Product.wxs` 에 제어판 프로그램 추가/제거용 `ARPPRODUCTICON` 프로퍼티와 바탕화면 바로가기 숏컷(`Shortcut` 요소)에 `Icon` 속성으로 `AppIcon.ico`를 매핑하고, 아이콘 파일도 설치 대상 본에 포함하여 완전하게 적용했습니다. 추가적으로 `Bundle.wxs``IconSourceFile="..\leak_test_project\EOL.ico"` 속성을 선언하여, 최종 배포 파일 자체의 아이콘도 해당 아이콘 이미지로 동일하게 적용했습니다.
4. **게시 경로 및 정화 스크립트 안전 조치 수립 (다운로드 폴더 파일 삭제 방지)**: Visual Studio 게시 프로필(`FolderProfile.pubxml`, `ClickOnceProfile.pubxml`)에 등록되어 있던 `PublishDir` / `PublishUrl` 경로가 `C:\Users\COMPUTER1\Downloads`로 설정되어 게시 정화 스크립트가 다운로드 폴더 내의 사용자의 기존 실행 파일(`*.exe`)들을 지우던 결함을 발견 및 조치하였습니다. 게시 프로필 경로를 `bin\Release\net9.0-windows\win-x86\publish\``C:\LeakTest\`로 변경하고, `.csproj` 내의 정화 로직(`FolderPublish` 타겟)이 오직 프로젝트 내부 빌드 결과물 경로(`SafePublishDir`)에 한해서만 실행되도록 엄격히 방어 로직을 수립했습니다.
5. **이전 Leak Test 명칭 및 C:\LeakTest 설치 경로 구성으로 완벽 원복**: 사용자 지시에 따라 EOL 변경 전 명칭 및 경로 체격으로 전면 롤백(Rollback)을 수행했습니다.
- 메인 어셈블리 및 실행 파일명: `leak_test_project.exe`
- 설치 및 배포 디렉터리: **`C:\LeakTest\`** 및 **`C:\ProgramData\LeakTest\`**
- 단일 설치 패키지 파일명: **`LeakTestSetup.exe`**
- 바탕화면 바로가기, 제어판 등록 이름 및 창 Title: **`Leak Test System`** / **`Pressure Leak Inspect System`**
6. **게시 출력 폴더 정화 로직 고도화 (오직 LeakTestSetup.exe 1개만 남도록 보장)**: 게시 완료 후 `publish\` 폴더 내에 `leak_test_project.exe`, `leak_test_project.pdb`, `libs` 서브 폴더 등이 파편으로 잔류하던 문제를 해결하기 위해, `CopyDllToPublish` 타겟의 실행 순서를 `BeforeTargets="FolderPublish"`로 변경하고 정화 대상을 프로젝트 내부 게시 폴더(`PublishDir` 및 `SafePublishDir`)의 모든 파일/폴더로 확충하였습니다. 이로써 게시 시 오직 최종 배포 단일 설치 파일인 **`LeakTestSetup.exe` 단 1개의 파일만 깔끔하게 보존**되도록 완벽히 조치했습니다.
7. **게시-설치 동작 전면 분리 및 독립 샌드박스 패키징 수립**:
- **게시(Publish) 시점**: 개발환경 C드라이브(`C:\LeakTest\`)로의 사전 파일 복사를 완전히 차단하고, 프로젝트 임시 샌드박스 경로(`temp_deploy\`)에서만 WiX MSI/Bundle 컴파일을 독립 수행하도록 개편했습니다. 게시 결과물 폴더(`publish\`)에는 파편 파일 없이 오직 단일 설치본인 **`LeakTestSetup.exe` 1개 파일만 깔끔하게 생성**됩니다.
- **설치(Install) 시점**: 최종 사용자가 단일 배포 파일인 `LeakTestSetup.exe`를 구동하여 설치를 진행하면, WiX 마법사를 통해 대상 PC의 C드라이브 **`C:\LeakTest\`** 폴더로 실행 파일 및 모든 설정/DLL 자원이 깔끔하게 추출 설치되도록 매핑을 완비했습니다.
8. **다운로드 폴더 기반 단일 EXE 게시 및 정화 로직 완벽 복원**: 사용자 지시에 따라 이전의 게시/설치 패키징 구조로 완벽히 원복하였습니다.
- **Visual Studio 게시 시**: 다운로드 폴더(`C:\Users\COMPUTER1\Downloads\`)로 게시가 수행되며, 게시 완료 직후 정화 스크립트가 `leak_test_project.exe`, `leak_test_project.pdb`, `libs` 서브 폴더 및 부속 파편 파일 일체를 다운로드 폴더에서 깨끗이 삭제하여 **오직 `LeakTestSetup.exe` 단 1개의 단일 설치 파일만 남도록 복구**했습니다.
- **설치 구동 시**: 다운로드 폴더에 생성된 `LeakTestSetup.exe`를 실행하면 **`C:\LeakTest\`** 경로에 프로그램 바이너리 및 설정/DLL이 설치되고, **바탕화면에 `Leak Test System` 바로가기 아이콘이 자동 생성**됩니다.

57
ob/06_Developer_Manual.md

@ -0,0 +1,57 @@
# 6. 개발자 전용 기술 사양서 (Developer Technical Specification)
본 문서는 프로그램의 내부 동작, 데이터 모델 바인딩 및 데이터베이스 매핑 구조를 담은 개발자 및 유지보수 엔지니어용 기술 문서입니다. 이 문서에 포함된 데이터들은 일반 사용자 매뉴얼에서 분리 이관되었습니다.
---
## 6.1 데이터 조회 화면 (Data View) UI-코드 매핑
데이터 조회 화면의 데이터 그리드(DataGrid)에 표시되는 컬럼 항목 및 C# 모델(`InspectData.cs`) 클래스의 바인딩 프로퍼티 정보는 다음과 같습니다.
### 데이터 그리드 바인딩 정보
| 화면 컬럼 헤더명 | 바인딩 프로퍼티 | 데이터베이스 매핑 필드 | 설명 |
|------|------|------|------|
| **IC_SN** | `IcSn` | `IC_SN` (PK/FK) | 제품 고유 IC SN 시리얼 번호 |
| **PCB_Barcode** | `PcbBarcode` | `PCB_Barcode` (UK) | PCB 바코드 고유 식별 정보 |
| **Maker** | `Maker` | `Maker` | 로그인 시 입력된 제조사명 |
| **Model** | `Model` | `Model` | 로그인 시 입력된 검사 모델명 |
| **Variant_1** | `Variant1` | `Variant_1` | 로그인 시 입력된 세부 파생 모델 1 |
| **Variant_2** | `Variant2` | `Variant_2` | 로그인 시 입력된 세부 파생 모델 2 |
| **Operator** | `Operator` | `Operator` | 작업 진행자 사용자 ID |
| **Production_Date** | `ProductionDate` | `Production_Date` | 검사 수행 및 완료 일시 (yyyy-MM-dd HH:mm:ss) |
| **Line** | `Line` | `Line` | 로그인 공정 작업 라인 |
| **Lot_No** | `LotNo` | `Lot_No` | 생산 Lot 번호 |
| **Jig_No** | `JigNo` | `Jig_No` | 사용 지그(Jig) 번호 |
| **Channel** | `Channel` | `Channel` | 검사 채널 (`LEFT` 또는 `RIGHT`) |
| **Spec_UL** | `SpecUL` | `Spec_UL` | 누설 상한 규격값 (sccm) |
| **Spec_LL** | `SpecLL` | `Spec_LL` | 누설 하한 규격값 (sccm) |
| **Leak_Value** | `MeasuredValue` | `Leak_Value` | 측정된 누설 리크량 |
| **Result** | `Judgment` | `Result` | 최종 판정 결과 (`OK` / `NG`) |
---
## 6.2 데이터베이스 테이블 및 컬럼 상세 스펙
검사 완료 시 데이터베이스 저장 및 쿼리에 사용되는 `[dbo].[EOL]` 테이블의 스키마 사양과 C# 데이터 모델의 매핑 내역은 다음과 같습니다.
### dbo.EOL 테이블 매핑 테이블
| DB 컬럼명 | 데이터 타입 | 매핑되는 C# 데이터 | 키 분류 / 제약사항 | 설명 |
| :--- | :--- | :--- | :--- | :--- |
| **`IC_SN`** | `NVARCHAR(10)` | `inspectData.IcSn` | **Primary Key / Foreign Key** | 제품의 16자리 IC 일련번호 |
| **`PCB_Barcode`** | `NVARCHAR(10)` | `inspectData.PcbBarcode` | **Unique Key** | 조회 매핑된 PCB 바코드 정보 |
| `Maker` | `NVARCHAR(10)` | `inspectData.Maker` | | 로그인 제조사 정보 |
| `Model` | `NVARCHAR(10)` | `inspectData.Model` | | 로그인 모델 정보 |
| `Variant_1` | `NVARCHAR(10)` | `inspectData.Variant1` | | 로그인 변형1 정보 |
| `Variant_2` | `NVARCHAR(10)` | `inspectData.Variant2` | | 로그인 변형2 정보 |
| `Operator` | `NVARCHAR(10)` | `inspectData.Operator` | | 로그인 검사자 ID |
| `Production_Date` | `DATETIME2(0)` | `inspectData.ProductionDate` | | 검사 수행 일시 (밀리초 제거 규격) |
| `Line` | `NVARCHAR(30)` | `inspectData.Line` | | 로그인 작업 라인 (기존 5자에서 30자로 확장됨) |
| `Lot_No` | `NVARCHAR(8)` | `inspectData.LotNo` | | 로그인 Lot 번호 |
| `Jig_No` | `NVARCHAR(10)` | `inspectData.JigNo` | | 로그인 지그 번호 |
| `Channel` | `NVARCHAR(5)` | `inspectData.Channel` | | 검사 채널 (`LEFT` / `RIGHT`) |
| `Spec_UL` | `DECIMAL(3,2)` | `inspectData.SpecUL` | | 규격 상한값 (소수점 2자리) |
| `Spec_LL` | `DECIMAL(3,2)` | `inspectData.SpecLL` | | 규격 하한값 (소수점 2자리) |
| `Leak_Value` | `DECIMAL(5,4)` | `inspectData.MeasuredValue` | | 측정된 릭 값 (소수점 4자리) |
| `Result` | `NVARCHAR(5)` | `inspectData.Judgment` | | 최종 판정 결과 (`OK` 또는 `NG`) |
Loading…
Cancel
Save