Browse Source

새 도면, 레이저 기계 최적화

master
seuuung 5 hours ago
parent
commit
64354c82a2
  1. BIN
      .vs/marking_gui.slnx/v18/.suo
  2. 40
      docs/developer_manual.md
  3. 13
      docs/knowledge_base.md
  4. 88
      marking_gui/MainWindow.xaml
  5. 598
      marking_gui/MainWindow.xaml.cs
  6. 14
      marking_gui/Manual/.obsidian/workspace.json
  7. 2
      marking_gui/Manual/02_초기_설정.md
  8. 10
      marking_gui/Manual/03_메인_화면.md
  9. 14
      marking_gui/Manual/07_시험_프로세스.md
  10. 2
      marking_gui/Manual/08_로그_관리.md
  11. 6
      marking_gui/Manual/09_오류_대응.md
  12. 22
      marking_gui/Manual/Manual_Merged.md
  13. 4
      marking_gui/Manual/사용자_메뉴얼.md
  14. 40
      marking_gui/PortSettingsWindow.xaml.cs
  15. 19
      marking_gui/QrVerificationWindow.xaml.cs
  16. 61
      marking_gui/Services/DatabaseService.cs
  17. 37
      marking_gui/Services/EquipmentMeasurementService.cs
  18. 73
      marking_gui/Services/EzdMarkingService.cs
  19. 5
      marking_gui/Services/IMarkingService.cs
  20. 5
      marking_gui/Services/LoginServicesStub.cs
  21. 29
      marking_gui/Services/MarkingConfigService.cs
  22. 3
      marking_gui/Setting.ini
  23. 27
      marking_gui/SettingsWindow.xaml.cs
  24. 7
      marking_gui/StartupLoginWindow.xaml
  25. 15
      marking_gui/StartupLoginWindow.xaml.cs

BIN
.vs/marking_gui.slnx/v18/.suo

Binary file not shown.

40
docs/developer_manual.md

@ -46,24 +46,22 @@ test=false
[Tags] [Tags]
; 도안(.ezd) 파일 내의 텍스트 오브젝트 치환용 태그명 매핑 리스트 ; 도안(.ezd) 파일 내의 텍스트 오브젝트 치환용 태그명 매핑 리스트
; (도안에 정의된 오브젝트명과 동일하게 설정해야 정상 작동합니다) ; (도안에 정의된 오브젝트명과 동일하게 설정해야 정상 작동합니다)
; ※ 아래 5개 태그만 치환됩니다. 불필요 태그는 사용하지 않습니다.
; 일련번호 (Serial Number) 치환 태그 ; 일련번호 4자리 순번 치환 태그 (예: 0001)
SerialTag=SERIAL SerialTag=SERIAL
; 작업 년월일 (Lot No) 치환 태그 ; 작업 Lot No 치환 태그 (로그인 입력값 YYMMDDLL 전체 전달, 예: 26080401)
LotNoTag=YYMMDD LotNoTag=YYMMDD
; 제품 모델명 (Product Name) 치환 태그 ; QR 코드 데이터 치환 태그 (IC_SN;도면품번;HKMC품번;각인번호; 포맷)
ProductTag=PRODUCT QrCodeTag=DATAMATRIX
; QR 코드 데이터 치환 태그 ; 도면 품번 치환 태그
QrCodeTag=QRCODETAG DrawingNoTag=PARTNO
; 검사 라인 코드 치환 태그 ; HKMC 품번 치환 태그
LineTag=LCODE HkmcNoTag=HKMCPO
; 작업 조 (Shift) 치환 태그
ShiftTag=BAN
; ================================================================= ; =================================================================
@ -188,18 +186,18 @@ Channel=1
- `IsActive` (BIT): 활성 상태 여부 - `IsActive` (BIT): 활성 상태 여부
- **`[dbo].[Marking]` (최종 성능 계측값 및 마킹 결과 저장 테이블)** - **`[dbo].[Marking]` (최종 성능 계측값 및 마킹 결과 저장 테이블)**
- `IC_SN` (VARCHAR(50), PRIMARY KEY): 센서 일련번호 - `IC_SN` (NVARCHAR(30), PRIMARY KEY, NOT NULL): 센서 IC 일련번호 (30자 초과 시 코드 내에서 절삭)
- `Marking_QR` (VARCHAR(100), UNIQUE): 고유 QR 코드 문자열 (yyMMdd + 일련번호) - `PCB_Barcode` (NVARCHAR(30), NOT NULL): PCB 바코드 — 정상 검사 시 스캔값, 강제검사(BYPASS) 시 `IC_SN` 값으로 대체 저장
- `Maker` (VARCHAR(50)), `Model` (VARCHAR(50)): 제품 사양 정보 - `Marking_QR` (NVARCHAR(60), NOT NULL): 각인 QR 코드 전체 문자열 (`IC_SN;도면품번;HKMC품번;각인번호;` 포맷, 60자 초과 시 절삭)
- `Variant_1`, `Variant_2` (VARCHAR(50)) - `Maker` (VARCHAR(10)), `Model` (VARCHAR(10)): 제품 사양 정보 (10자 초과 시 코드 내에서 절삭)
- `Operator` (VARCHAR(50)): 담당 작업자 ID - `Variant_1`, `Variant_2` (VARCHAR(10)): 제품 변형 코드 (10자 초과 시 절삭)
- `Operator` (VARCHAR(10)): 담당 작업자 ID (10자 초과 시 절삭)
- `Production_Date` (DATETIME): 생산 일자 및 계측 시간 - `Production_Date` (DATETIME): 생산 일자 및 계측 시간
- `Line` (VARCHAR(10)): 생산 라인 (최대 10자 제한, 초과 시 코드 내에서 절삭) - `Line` (VARCHAR(10)): 생산 라인 (최대 10자 제한, 초과 시 코드 내에서 절삭)
- `Lot_No` (VARCHAR(8)): 로트 번호 (최대 8자 제한, 초과 시 코드 내에서 절삭) - `Lot_No` (VARCHAR(10)): 로트 번호 — `YYMMDDLL` 형식 입력 (최대 10자 제한, 초과 시 코드 내에서 절삭)
- `Jig_No` (VARCHAR(10)): 지그 번호 - `Jig_No` (VARCHAR(10)): 지그 번호 (10자 초과 시 절삭)
- `PT_Vol_1` (DECIMAL(18,2)), `PT_Current_1` (DECIMAL(18,2)), `PT_Resistance_1` (DECIMAL(18,3)): 실측 전압, 전류, 저항값 - `PT_Vol_1` (DECIMAL(4,2)), `PT_Current_1` (DECIMAL(4,2)), `PT_Resistance_1` (DECIMAL(5,3)): 실측 전압(V), 전류(mA), 저항(kΩ) — 소수점 이하 2~3자리로 반올림 적재
- `Result` (VARCHAR(10)): 최종 종합 판정 (`PASS` 또는 `FAIL`) - `Result` (VARCHAR(10)): 최종 종합 판정 — **`OK`** (QR 검증 통과) 또는 **`NG`** (불합격 또는 QR 검증 실패)
- `MarkedSerial` (VARCHAR(50)): 최종 마킹된 시퀀스 코드
### 3.2 이전 공정 결과 교차 검증 (Subquery) ### 3.2 이전 공정 결과 교차 검증 (Subquery)
검사 개시 시점, 제품의 S/N(`IC_SN`)을 기반으로 선행 공정이 완료되었는지 조회합니다. 검사 개시 시점, 제품의 S/N(`IC_SN`)을 기반으로 선행 공정이 완료되었는지 조회합니다.

13
docs/knowledge_base.md

@ -31,7 +31,7 @@
전압과 전류에 국한되었던 기존 시스템에 저항 검사 프로세스를 추가하여 3대 전기 파라미터를 완성했습니다. 전압과 전류에 국한되었던 기존 시스템에 저항 검사 프로세스를 추가하여 3대 전기 파라미터를 완성했습니다.
- **물리적 개연성 반영:** V = I * R 연계를 고려하여, 전압(4~6V) 및 전류(8~10A) 조건에서 저항 기본 범위를 **0.4Ω ~ 0.8Ω**으로 세팅했습니다. - **물리적 개연성 반영:** V = I * R 연계를 고려하여, 전압(4~6V) 및 전류(8~10A) 조건에서 저항 기본 범위를 **0.4Ω ~ 0.8Ω**으로 세팅했습니다.
- **UI 및 환경설정 연동:** 메인 화면에 세 번째 Column으로 `측정 저항 (Resistance)` 카드를 시각적으로 추가하고 설정 창(SettingsWindow)의 세로 크기를 늘려 저항 판정 임계치 입력란을 통합했습니다. - **UI 및 환경설정 연동:** 메인 화면에 세 번째 Column으로 `측정 저항 (Resistance)` 카드를 시각적으로 추가하고 설정 창(SettingsWindow)의 세로 크기를 늘려 저항 판정 임계치 입력란을 통합했습니다.
- **판정 결합:** `isFinalPass = isVoltageOk && isCurrentOk && isResistanceOk` 조건을 통해 세 항목 중 하나라도 이탈 시 자동으로 빨간색 텍스트 및 대형 FAIL 판정이 출력되도록 비즈니스 로직을 보강했습니다. - **판정 결합:** `isFinalPass = isVoltageOk && isCurrentOk && isResistanceOk` 조건을 통해 세 항목 중 하나라도 이탈 시 자동으로 빨간색 텍스트 및 대형 **`NG`** 판정이 출력되도록 비즈니스 로직을 보강했습니다.
### 2.2. 로그인 저장 프로시저 호출 연동 ### 2.2. 로그인 저장 프로시저 호출 연동
인라인 SQL 질의 구조를 탈피하여 외부 설정에 명시된 SQL Server 저장 프로시저를 직접 호출하도록 구현했습니다. 인라인 SQL 질의 구조를 탈피하여 외부 설정에 명시된 SQL Server 저장 프로시저를 직접 호출하도록 구현했습니다.
@ -105,10 +105,13 @@
- 작업자의 고유 ID(`OperatorId`), 패스워드(`Password`), 이름(`Name`) 및 계정 상태(`IsActive`)를 관리합니다. - 작업자의 고유 ID(`OperatorId`), 패스워드(`Password`), 이름(`Name`) 및 계정 상태(`IsActive`)를 관리합니다.
- 로그인 조회 속도를 향상시키기 위해 `OperatorId` 컬럼에 UNIQUE 제약 조건을 할당했습니다. - 로그인 조회 속도를 향상시키기 위해 `OperatorId` 컬럼에 UNIQUE 제약 조건을 할당했습니다.
- **[dbo].[Marking] (공정 메타데이터 및 성능 계측값 저장 테이블):** - **[dbo].[Marking] (공정 메타데이터 및 성능 계측값 저장 테이블):**
- 바코드 S/N(`IC_SN`, 기본키)을 키로 하며, 고유 식별을 위해 메타데이터와 날짜 조합의 고유 키 `Marking_QR` (Unique Key)이 생성됩니다. - 바코드 S/N(`IC_SN`, NVARCHAR(30), 기본키, NOT NULL)을 키로 하며, 30자 초과 시 코드 내에서 절삭합니다.
- 공정 정보(`Maker`, `Model`, `Variant_1`, `Variant_2`, `Operator`, `Production_Date`, `Line`, `Lot_No`, `Jig_No`) 및 최종 판정(`Result`)이 통합 적재됩니다. - `PCB_Barcode` (NVARCHAR(30), NOT NULL): 정상 검사 시 PCB 바코드 스캔값, 강제검사(BYPASS) 시 `IC_SN` 값으로 자동 대체 저장됩니다.
- 3단계에 걸친 전압/전류/저항 컬럼(`PT_Vol_1~3`, `PT_Current_1~3`, `PT_Resistance_1~3`)이 존재하며, 현재는 1번 컬럼들에 최종 계측 수치(소수점 2자리 반올림)를 저장하고 2, 3번 컬럼은 `0.00`으로 기본값을 보정하여 저장합니다. - 고유 QR 코드 문자열 `Marking_QR` (NVARCHAR(60)): `IC_SN;도면품번;HKMC품번;각인번호;` 포맷으로 구성되며, 60자 초과 시 절삭합니다.
- DB 필드 크기 한계를 초과하여 적재 에러가 나지 않도록, `Line` 컬럼은 최대 5자, `Lot_No` 컬럼은 최대 8자 등으로 자르는(Truncate) 방어적 코드 처리가 수행됩니다. - 공정 정보(`Maker`, `Model`, `Variant_1`, `Variant_2`, `Operator`, `Production_Date`, `Line`, `Lot_No`, `Jig_No`) 및 최종 판정(`Result`)이 통합 적재됩니다. 각 문자열 컬럼은 최대 10자로 초과 시 절삭 처리됩니다.
- 3단계에 걸친 전압/전류/저항 컬럼(`PT_Vol_1~3`, `PT_Current_1~3`, `PT_Resistance_1~3`)이 존재하며, 현재는 1번 컬럼들에 최종 계측 수치(소수점 2~3자리 반올림)를 저장하고 2, 3번 컬럼은 `0.00`으로 기본값을 보정하여 저장합니다.
- `Result` 컬럼에는 **`OK`** (QR 검증 통과 시) 또는 **`NG`** (불합격 또는 QR 검증 실패 시) 두 가지 값만 저장됩니다.
- `Lot_No` 컬럼은 `YYMMDDLL` 형식(예: `26080401`)으로 입력되며 최대 **10자**까지 허용됩니다.
### 4.2. 로그인 검증 저장 프로시저 ([CheckOperator]) ### 4.2. 로그인 검증 저장 프로시저 ([CheckOperator])
- **프로시저 기능:** 입력된 ID와 패스워드가 활성 상태인 작업자 테이블에 매칭되는지 대소문자 구분 없이 카운팅합니다. - **프로시저 기능:** 입력된 ID와 패스워드가 활성 상태인 작업자 테이블에 매칭되는지 대소문자 구분 없이 카운팅합니다.

88
marking_gui/MainWindow.xaml

@ -11,6 +11,12 @@
WindowStartupLocation="CenterScreen" Background="#F3F4F6" WindowStartupLocation="CenterScreen" Background="#F3F4F6"
ContentRendered="Window_ContentRendered"> ContentRendered="Window_ContentRendered">
<Window.Resources> <Window.Resources>
<!-- 기본 버튼 포커스 비주얼 및 깜빡임 잔상 제거 -->
<Style TargetType="Button">
<Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
</Style>
<!-- 버튼 스타일 --> <!-- 버튼 스타일 -->
<Style x:Key="ModernButtonStyle" TargetType="Button"> <Style x:Key="ModernButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#3B82F6"/> <Setter Property="Background" Value="#3B82F6"/>
@ -18,6 +24,7 @@
<Setter Property="FontSize" Value="16"/> <Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="Bold"/> <Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Focusable" Value="False"/> <Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="15,10"/> <Setter Property="Padding" Value="15,10"/>
<Setter Property="Template"> <Setter Property="Template">
@ -50,6 +57,7 @@
<Setter Property="FontSize" Value="24"/> <Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Bold"/> <Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Focusable" Value="False"/> <Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
@ -71,6 +79,74 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<!-- 중지 버튼 전용 스타일 (스타트 버튼과 동일한 24pt 폰트 크기) -->
<Style x:Key="StopButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#EF4444"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#DC2626"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="Foreground" Value="#9CA3AF"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 심플 DB 재연결 전용 버튼 스타일 -->
<Style x:Key="SimpleRefreshButtonStyle" TargetType="Button">
<Setter Property="Background" Value="#F3F4F6"/>
<Setter Property="Foreground" Value="#2563EB"/>
<Setter Property="BorderBrush" Value="#3B82F6"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="Padding" Value="8,3"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4"
SnapsToDevicePixels="True"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#DBEAFE"/>
<Setter Property="BorderBrush" Value="#1D4ED8"/>
<Setter Property="Foreground" Value="#1D4ED8"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="#E5E7EB"/>
<Setter Property="BorderBrush" Value="#9CA3AF"/>
<Setter Property="Foreground" Value="#6B7280"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 카드 보더 스타일 --> <!-- 카드 보더 스타일 -->
<Style x:Key="CardBorderStyle" TargetType="Border"> <Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="White"/> <Setter Property="Background" Value="White"/>
@ -88,6 +164,7 @@
<Setter Property="FontSize" Value="12"/> <Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="Bold"/> <Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Focusable" Value="False"/> <Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="10,5"/> <Setter Property="Padding" Value="10,5"/>
<Setter Property="Template"> <Setter Property="Template">
@ -120,6 +197,7 @@
<Setter Property="FontSize" Value="24"/> <Setter Property="FontSize" Value="24"/>
<Setter Property="FontWeight" Value="Bold"/> <Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Focusable" Value="False"/> <Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
<Setter Property="BorderThickness" Value="0"/> <Setter Property="BorderThickness" Value="0"/>
<Setter Property="Template"> <Setter Property="Template">
<Setter.Value> <Setter.Value>
@ -265,7 +343,11 @@
</StackPanel> </StackPanel>
<!-- 다음 각인 카운트 --> <!-- 다음 각인 카운트 -->
<StackPanel Grid.Column="1" Margin="10,0,0,0"> <StackPanel Grid.Column="1" Margin="10,0,0,0">
<TextBlock Text="다음 각인 순번" FontSize="14" Foreground="#9CA3AF" FontWeight="SemiBold"/> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="다음 각인 순번" FontSize="14" Foreground="#9CA3AF" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Button x:Name="btnRefreshSequence" Content="🔄 DB 재연결" Visibility="Collapsed" Margin="6,0,0,0" Padding="8,3" FontSize="11"
Style="{StaticResource SimpleRefreshButtonStyle}" Click="btnRefreshSequence_Click" ToolTip="DB 서버 다시 연결"/>
</StackPanel>
<TextBlock x:Name="txtNextSequence" Text="--- 차례" FontSize="22" FontWeight="Bold" Foreground="#2563EB" Margin="0,2,0,0"/> <TextBlock x:Name="txtNextSequence" Text="--- 차례" FontSize="22" FontWeight="Bold" Foreground="#2563EB" Margin="0,2,0,0"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -554,8 +636,8 @@
<!-- 3. 하단 푸터 (전체 영역이 스타트 버튼) --> <!-- 3. 하단 푸터 (전체 영역이 스타트 버튼) -->
<Border Grid.Row="2" BorderBrush="#D1D5DB" BorderThickness="0,1,0,0"> <Border Grid.Row="2" BorderBrush="#D1D5DB" BorderThickness="0,1,0,0">
<Button x:Name="btnStart" Content="▶ 검사 시작 (START)" <Button x:Name="btnStart" Content="⏳ DB 연결 및 순번 조회 중..."
Style="{StaticResource StartButtonStyle}" IsEnabled="True" Click="btnStart_Click" Style="{StaticResource StartButtonStyle}" IsEnabled="False" Click="btnStart_Click"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/> HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border> </Border>

598
marking_gui/MainWindow.xaml.cs

@ -38,6 +38,12 @@ namespace marking_gui
// 계측기 연동 서비스 (DMM 전압 + Power Supply 전류) // 계측기 연동 서비스 (DMM 전압 + Power Supply 전류)
private EquipmentMeasurementService _equipment; private EquipmentMeasurementService _equipment;
// 검사 취소 토큰 및 진행 상태 관리
private CancellationTokenSource? _inspectionCts;
private bool _isInspectionRunning = false;
private bool _isRestoringDevices = false;
private bool _isInitialLoading = true;
// 검사 시작 / 종료 시간 추적 // 검사 시작 / 종료 시간 추적
private DateTime _testStartTime = DateTime.MinValue; private DateTime _testStartTime = DateTime.MinValue;
private DateTime _testEndTime = DateTime.MinValue; private DateTime _testEndTime = DateTime.MinValue;
@ -82,6 +88,9 @@ namespace marking_gui
_dbWatcherCts = new CancellationTokenSource(); _dbWatcherCts = new CancellationTokenSource();
StartDbReconnectWatcher(_dbWatcherCts.Token); StartDbReconnectWatcher(_dbWatcherCts.Token);
} }
// 초기 로딩 시점 스타트 버튼 비활성화 텍스트 표기 보장
SetStartButtonConnecting("⏳ DB 연결 및 순번 조회 중...");
} }
private void ApplyTestModeSettings() private void ApplyTestModeSettings()
@ -351,8 +360,26 @@ namespace marking_gui
txtResistanceRangeGuide.Text = string.Format("기준범위: {0:F3}kΩ ~ {1:F3}kΩ", _limits.MinResistance, _limits.MaxResistance); txtResistanceRangeGuide.Text = string.Format("기준범위: {0:F3}kΩ ~ {1:F3}kΩ", _limits.MinResistance, _limits.MaxResistance);
} }
private bool CheckUserLoggedIn()
{
if (_loginInfo == null || string.IsNullOrWhiteSpace(_loginInfo.Operator) || _loginInfo.Operator == "---")
{
MessageBox.Show("작업자 로그인 후 이용 가능합니다.", "로그인 필요", MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
return true;
}
private void btnOpenSettings_Click(object sender, RoutedEventArgs e) private void btnOpenSettings_Click(object sender, RoutedEventArgs e)
{ {
if (!CheckUserLoggedIn()) return;
if (_isInspectionRunning)
{
MessageBox.Show("현재 검사가 진행 중입니다.\n검사가 완료되거나 중지된 후 설정을 변경하십시오.", "설정 변경 불가", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
SettingsWindow settings = new SettingsWindow(_limits); SettingsWindow settings = new SettingsWindow(_limits);
settings.Owner = this; settings.Owner = this;
@ -368,6 +395,14 @@ namespace marking_gui
private void btnOpenPortSettings_Click(object sender, RoutedEventArgs e) private void btnOpenPortSettings_Click(object sender, RoutedEventArgs e)
{ {
if (!CheckUserLoggedIn()) return;
if (_isInspectionRunning)
{
MessageBox.Show("현재 검사가 진행 중입니다.\n검사가 완료되거나 중지된 후 포트 설정을 변경하십시오.", "포트 설정 변경 불가", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
PortSettingsWindow portSettings = new PortSettingsWindow(); PortSettingsWindow portSettings = new PortSettingsWindow();
portSettings.Owner = this; portSettings.Owner = this;
@ -381,22 +416,198 @@ namespace marking_gui
} }
// ── 버튼 상태 헬퍼 ──────────────────────────────────────────────── // ── 버튼 상태 헬퍼 ────────────────────────────────────────────────
/// <summary>검사 진행 중 → 버튼 비활성화 + 텍스트 변경</summary> /// <summary>검사 진행 중 → 버튼을 [중지] 버튼으로 전환</summary>
private void SetStartButtonBusy() private void SetStartButtonBusy()
{ {
btnStart.IsEnabled = false; _isInspectionRunning = true;
btnStart.Content = "⏳ 검사 진행 중..."; if (btnStart != null)
{
btnStart.IsEnabled = true;
btnStart.Content = "⏹ 검사 중지 (STOP)";
try
{
var stopStyle = FindResource("StopButtonStyle") as Style;
if (stopStyle != null) btnStart.Style = stopStyle;
}
catch { }
}
if (btnBypassStart != null) btnBypassStart.IsEnabled = false; if (btnBypassStart != null) btnBypassStart.IsEnabled = false;
} }
/// <summary>검사 완료/취소 → 버튼 활성화 + 텍스트 원복</summary> /// <summary>장비 복구 진행 중 → 버튼 비활성화 + 시작 차단</summary>
private void SetStartButtonRestoring()
{
_isInspectionRunning = true;
if (btnStart != null)
{
btnStart.IsEnabled = false;
btnStart.Content = "⏳ 장비 복구 중...";
try
{
var stopStyle = FindResource("StopButtonStyle") as Style;
if (stopStyle != null) btnStart.Style = stopStyle;
}
catch { }
}
if (btnBypassStart != null) btnBypassStart.IsEnabled = false;
}
/// <summary>DB 또는 장치 연결 시도/실패 중 → 스타트 버튼 비활성화 + 지정 텍스트 표기</summary>
private void SetStartButtonConnecting(string statusText = "⚠️ DB 연결 필요")
{
if (_isRestoringDevices) return;
if (btnStart != null)
{
btnStart.IsEnabled = false;
btnStart.Content = statusText;
try
{
var stopStyle = FindResource("StopButtonStyle") as Style;
if (stopStyle != null) btnStart.Style = stopStyle;
}
catch { }
}
if (btnBypassStart != null) btnBypassStart.IsEnabled = false;
}
/// <summary>검사 완료/취소/대기 → 버튼을 [시작] 버튼으로 복구 (DB 미연결 시 ⚠️ DB 연결 필요 표기 유지)</summary>
private void SetStartButtonReady() private void SetStartButtonReady()
{ {
btnStart.IsEnabled = true; // 장비 복구 수순이 진행 중이거나 초기 로딩 중인 동안에는 Ready 상태로 전환 차단
btnStart.Content = "▶ 검사 시작 (START)"; if (_isRestoringDevices || _isInitialLoading) return;
// 만약 오프라인 모드가 아닌데 DB가 연결되어 있지 않은 상태라면 검사 시작 버튼을 '⚠️ DB 연결 필요' 상태로 계속 유지
if (_database != null && !_database.OfflineMode && _dbWasOffline)
{
_isInspectionRunning = false;
if (btnStart != null)
{
btnStart.IsEnabled = false;
btnStart.Content = "⚠️ DB 연결 필요";
try
{
var stopStyle = FindResource("StopButtonStyle") as Style;
if (stopStyle != null) btnStart.Style = stopStyle;
}
catch { }
}
if (btnBypassStart != null) btnBypassStart.IsEnabled = false;
return;
}
_isInspectionRunning = false;
if (btnStart != null)
{
btnStart.IsEnabled = true;
btnStart.Content = "▶ 검사 시작 (START)";
try
{
var startStyle = FindResource("StartButtonStyle") as Style;
if (startStyle != null) btnStart.Style = startStyle;
}
catch { }
}
if (btnBypassStart != null) btnBypassStart.IsEnabled = true; if (btnBypassStart != null) btnBypassStart.IsEnabled = true;
} }
/// <summary>사용자가 [STOP] 버튼을 클릭했을 때 검사 중단 및 장비 정상 상태 리셋</summary>
private async System.Threading.Tasks.Task CancelInspectionSequenceAsync()
{
if (_isRestoringDevices) return;
_isRestoringDevices = true; // 복구 잠금 시작
LoggerService.Warn("[검사 중지] 사용자가 [STOP] 검사 중지 버튼을 클릭했습니다.");
// 장비 복구 작업 완료 시까지 버튼 클릭 차단 (비활성화)
SetStartButtonRestoring();
if (txtProgressStatus != null)
{
txtProgressStatus.Text = "검사 중지 중 (장비 복구 진행 중...)";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
}
if (txtSystemStatus != null)
{
txtSystemStatus.Text = "상태: 검사 중지 요청됨 - 장비 초기화 중...";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
}
try
{
_inspectionCts?.Cancel();
}
catch (Exception ex)
{
LoggerService.Warn($"CancellationTokenSource 취소 중 예외: {ex.Message}");
}
await ResetDevicesToReadyStateAsync();
// 장비 복구 완결 후 비로소 잠금 해제
_isRestoringDevices = false;
SetStartButtonReady();
if (txtSerial != null) txtSerial.IsEnabled = true;
if (txtProgressStatus != null)
{
txtProgressStatus.Text = "대기 중";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(107, 114, 128)); // Gray
}
if (txtSystemStatus != null)
{
txtSystemStatus.Text = "상태: 대기 중";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
}
}
/// <summary>DMM 계측기 및 레이저 마킹 장비를 재시작 가능한 정상 초기 상태로 복구</summary>
private async System.Threading.Tasks.Task ResetDevicesToReadyStateAsync()
{
try
{
// 1. PSU 파워서플라이 출력 강제 OFF (측정 중단 시에도 출력이 켜진 상태로 남지 않도록 보장)
if (_equipment != null)
{
await System.Threading.Tasks.Task.Run(async () =>
{
try
{
await _equipment.ResetAsync();
LoggerService.Info("[장비 리셋] DMM / PSU 출력 OFF 정상 완료");
}
catch (Exception ex)
{
LoggerService.Warn($"[장비 리셋] DMM/PSU 복구 예외: {ex.Message}");
}
});
}
// 2. 레이저 마킹 장비 중단 및 핸들 락 해제
if (_markingService != null)
{
await System.Threading.Tasks.Task.Run(() =>
{
try
{
_markingService.StopMarking();
LoggerService.Info("[장비 리셋] 레이저 마킹 장비 StopMarking 호출 완료");
}
catch (Exception ex)
{
LoggerService.Warn($"[장비 리셋] 마킹 장비 중단 예외: {ex.Message}");
}
});
}
}
catch (Exception ex)
{
LoggerService.Error("[장비 복구 오류] 장비 복구 처리 중 예외 발생", ex);
}
}
// UI 초기화 상태 // UI 초기화 상태
private void ResetUI() private void ResetUI()
{ {
@ -493,6 +704,15 @@ namespace marking_gui
// 스타트 버튼 클릭 // 스타트 버튼 클릭
private async void btnStart_Click(object sender, RoutedEventArgs e) private async void btnStart_Click(object sender, RoutedEventArgs e)
{ {
// 장비 복구 중인 경우 버튼 클릭 완전 금지
if (_isRestoringDevices) return;
if (_isInspectionRunning)
{
await CancelInspectionSequenceAsync();
return;
}
await RunInspectionSequenceAsync(bypassPrevSteps: false); await RunInspectionSequenceAsync(bypassPrevSteps: false);
} }
@ -505,6 +725,8 @@ namespace marking_gui
// 스캔 테스트 버튼 클릭 (바코드 / QR 스캐너 연동 독립 테스트) // 스캔 테스트 버튼 클릭 (바코드 / QR 스캐너 연동 독립 테스트)
private void btnScanTest_Click(object sender, RoutedEventArgs e) private void btnScanTest_Click(object sender, RoutedEventArgs e)
{ {
if (!CheckUserLoggedIn()) return;
string testSampleQr = "TEST_SN_12345;DRAWING_SAMPLE;HKMC_SAMPLE;2607299999;"; string testSampleQr = "TEST_SN_12345;DRAWING_SAMPLE;HKMC_SAMPLE;2607299999;";
var qrTestWin = new QrVerificationWindow(testSampleQr, _barcodeScanner) var qrTestWin = new QrVerificationWindow(testSampleQr, _barcodeScanner)
{ {
@ -526,13 +748,18 @@ namespace marking_gui
private async System.Threading.Tasks.Task RunInspectionSequenceAsync(bool bypassPrevSteps) private async System.Threading.Tasks.Task RunInspectionSequenceAsync(bool bypassPrevSteps)
{ {
// 진입하자마자 즉각 버튼을 비활성화하여 중복 클릭 원천 차단 (비동기 대기 이전에 호출 필수) _inspectionCts?.Dispose();
_inspectionCts = new CancellationTokenSource();
var token = _inspectionCts.Token; // 이 시퀀스 전체에서 공유할 취소 토큰
// 진입 즉시 버튼을 [⏹ 검사 중지 (STOP)] 상태로 변경 (사용자 요청 반영)
SetStartButtonBusy(); SetStartButtonBusy();
// 0. 각인 일련번호 사전 초과 검증 (9999 초과 시 테스트 시작 차단) // 0. 각인 일련번호 사전 초과 검증 (9999 초과 시 테스트 시작 차단)
int checkSeq; int checkSeq;
try try
{ {
token.ThrowIfCancellationRequested();
checkSeq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo); checkSeq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo);
} }
catch (Exception seqEx) catch (Exception seqEx)
@ -575,7 +802,7 @@ namespace marking_gui
// 검사 시작 누르자마자 이전의 측정 결과 및 상태 화면을 즉시 클리어 // 검사 시작 누르자마자 이전의 측정 결과 및 상태 화면을 즉시 클리어
ResetUI(); ResetUI();
// ResetUI 내부에서 버튼이 다시 활성화(Ready)되므로 Busy 상태로 잠금 유지 // ResetUI 실행 후에도 버튼 상태를 [⏹ 검사 중지 (STOP)] 유지
SetStartButtonBusy(); SetStartButtonBusy();
// 검사 시작 즉시 IC_SN 입력란을 초기화 (이전 S/N이 남지 않도록) // 검사 시작 즉시 IC_SN 입력란을 초기화 (이전 S/N이 남지 않도록)
@ -646,6 +873,7 @@ namespace marking_gui
bool isDbConnected = true; bool isDbConnected = true;
if (!bypassPrevSteps) if (!bypassPrevSteps)
{ {
token.ThrowIfCancellationRequested();
txtSystemStatus.Text = "상태: DB 연결 확인 중..."; txtSystemStatus.Text = "상태: DB 연결 확인 중...";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
if (txtProgressStatus != null) if (txtProgressStatus != null)
@ -654,7 +882,7 @@ namespace marking_gui
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
} }
isDbConnected = await System.Threading.Tasks.Task.Run(() => _database.CheckConnection()); isDbConnected = await System.Threading.Tasks.Task.Run(() => _database.CheckConnection(), token);
} }
if (!bypassPrevSteps && !isDbConnected) if (!bypassPrevSteps && !isDbConnected)
@ -677,6 +905,7 @@ namespace marking_gui
// ── STEP 3. UI 상태 갱신 및 이전 공정 조회 준비 ───────────────────── // ── STEP 3. UI 상태 갱신 및 이전 공정 조회 준비 ─────────────────────
// 새로운 검사 진행을 위해 UI 상태를 초기화 // 새로운 검사 진행을 위해 UI 상태를 초기화
ResetUI(); ResetUI();
SetStartButtonBusy();
// UI 상태 복원 및 진행 개시 알림 // UI 상태 복원 및 진행 개시 알림
txtSerial.Text = serial; txtSerial.Text = serial;
@ -701,7 +930,8 @@ namespace marking_gui
{ {
try try
{ {
product = await System.Threading.Tasks.Task.Run(() => _database.GetProductBySerial(serial)); token.ThrowIfCancellationRequested();
product = await System.Threading.Tasks.Task.Run(() => _database.GetProductBySerial(serial), token);
_currentProduct = product; // EOL PCB 바코드 포함 저장 _currentProduct = product; // EOL PCB 바코드 포함 저장
} }
catch (Exception ex) catch (Exception ex)
@ -883,7 +1113,8 @@ namespace marking_gui
if (shouldProceed) if (shouldProceed)
{ {
// ── STEP 4. IC S/N DB 중복 여부 검사 (재작업 확인 팝업) ──────────────── // ── STEP 4. IC S/N DB 중복 여부 검사 (재작업 확인 팝업) ────────────────
bool isDuplicate = await System.Threading.Tasks.Task.Run(() => _database.IsProductExists(serial)); token.ThrowIfCancellationRequested();
bool isDuplicate = await System.Threading.Tasks.Task.Run(() => _database.IsProductExists(serial), token);
if (isDuplicate) if (isDuplicate)
{ {
LoggerService.Warn($"[재작업 감지] IC S/N ({serial})가 이미 DB에 존재합니다. 작업자 확인 팝업 표시"); LoggerService.Warn($"[재작업 감지] IC S/N ({serial})가 이미 DB에 존재합니다. 작업자 확인 팝업 표시");
@ -955,11 +1186,18 @@ namespace marking_gui
try try
{ {
var measurement = await ReadMeasurementFromDeviceAsync(); token.ThrowIfCancellationRequested();
var measurement = await ReadMeasurementFromDeviceAsync(token);
token.ThrowIfCancellationRequested(); // 계측 완료 후에도 즉시 취소 확인
finalVoltage = measurement.Voltage; finalVoltage = measurement.Voltage;
finalCurrent = measurement.Current; finalCurrent = measurement.Current;
finalResistance = measurement.Resistance; finalResistance = measurement.Resistance;
} }
catch (OperationCanceledException)
{
LoggerService.Warn("[검사 중지] 계측 단계에서 중지 요청이 수신되었습니다.");
return; // CancelInspectionSequenceAsync가 장비 복구를 담당
}
catch (Exception measEx) catch (Exception measEx)
{ {
LoggerService.Error("계측기 데이터 수집 중 예외 발생", measEx); LoggerService.Error("계측기 데이터 수집 중 예외 발생", measEx);
@ -1014,26 +1252,27 @@ namespace marking_gui
if (isFinalPass) if (isFinalPass)
{ {
txtSystemStatus.Text = "상태: 검사 완료 (합격)"; txtSystemStatus.Text = "상태: 전기적 계측 합격 (각인/스캔 대기)";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
txtFinalResult.Text = "OK"; // 최종 OK 표기는 스캔 검증까지 다 성공했을 때 처리되도록 대기 상태 유지
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green text for white card txtFinalResult.Text = "대기 중";
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(156, 163, 175)); // Gray
if (borderVerdictCard != null) if (borderVerdictCard != null)
{ {
borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(240, 253, 250)); // Light Green background borderVerdictCard.Background = System.Windows.Media.Brushes.White;
borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green border borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(229, 231, 235));
borderVerdictCard.BorderThickness = new Thickness(3); borderVerdictCard.BorderThickness = new Thickness(1);
} }
if (badgeFinalResult != null) if (badgeFinalResult != null)
{ {
badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white badgeFinalResult.Background = System.Windows.Media.Brushes.White;
} }
borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246));
if (txtProgressStatus != null) if (txtProgressStatus != null)
{ {
txtProgressStatus.Text = "제품 합격 - 레이저 각인 중..."; txtProgressStatus.Text = "계측 합격 - 레이저 각인 및 QR 스캔 진행 중...";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange text txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange text
} }
} }
@ -1072,23 +1311,35 @@ namespace marking_gui
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 중..."; if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 중...";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange
_ = System.Threading.Tasks.Task.Run(async () => // fire-and-forget 대신 await로 대기하여 취소 토큰이 즉시 반영되도록 변경
await System.Threading.Tasks.Task.Run(async () =>
{ {
try try
{ {
// 1. 각인 일련번호 조회 및 생성 (YYMMDDLL + 4자리 시퀀스) // 마킹 진입 전 취소 토큰 최종 확인
int seq; token.ThrowIfCancellationRequested();
try
// 1. 각인 일련번호 생성 (검사 시작 전 화면에 표시된 순번 그대로 사용)
int seq = 1;
if (txtNextSequence != null)
{ {
seq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo); Dispatcher.Invoke(() =>
{
string rawText = txtNextSequence.Text;
string digitsOnly = System.Text.RegularExpressions.Regex.Replace(rawText ?? "", @"[^\d]", "");
if (int.TryParse(digitsOnly, out int parsedSeq) && parsedSeq > 0)
{
seq = parsedSeq;
}
});
} }
catch (Exception seqEx)
if (seq > 9999)
{ {
LoggerService.Error("[순번 조회 실패] 마킹 직전 DB 순번 조회 실패", seqEx);
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
string errMsg = $"각인 순번 DB 조회 실패: {seqEx.Message}"; string errMsg = "마킹 직전 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오.";
if (txtMarkingStatus != null) txtMarkingStatus.Text = "순번 조회 실패"; if (txtMarkingStatus != null) txtMarkingStatus.Text = "순번 초과";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
if (txtSystemStatus != null) if (txtSystemStatus != null)
{ {
@ -1096,21 +1347,22 @@ namespace marking_gui
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
} }
if (txtErrorMessage != null) txtErrorMessage.Text = errMsg; if (txtErrorMessage != null) txtErrorMessage.Text = errMsg;
if (borderErrorBanner != null) borderErrorBanner.Visibility = System.Windows.Visibility.Visible; if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
if (borderNormalStatus != null) borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed; if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Collapsed;
if (txtProgressStatus != null) if (txtProgressStatus != null)
{ {
txtProgressStatus.Text = "마킹 실패 (순번 조회 불가)"; txtProgressStatus.Text = "마킹 실패 (순번 초과)";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
} }
MessageBox.Show($"각인 순번을 DB에서 가져오지 못했습니다.\n\n{seqEx.Message}\n\nDB 연결 상태를 확인하십시오.", "순번 조회 실패", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show(errMsg, "순번 초과 오류", MessageBoxButton.OK, MessageBoxImage.Error);
SetStartButtonReady();
txtSerial.IsEnabled = true;
}); });
return; return;
} }
string yymmdd = DateTime.Now.ToString("yyMMdd"); string lotNo = string.IsNullOrWhiteSpace(_loginInfo.LotNo) ? "00" : _loginInfo.LotNo.Trim();
string lotNo = string.IsNullOrWhiteSpace(_loginInfo.LotNo) ? "00" : (_loginInfo.LotNo.Trim().Length < 2 ? _loginInfo.LotNo.Trim().PadLeft(2, '0') : _loginInfo.LotNo.Trim()); string markedSerial = string.Format("{0}{1:D4}", lotNo, seq);
string markedSerial = string.Format("{0}{1}{2:D4}", yymmdd, lotNo, seq);
string drawingNo = _markingConfig.DrawingNo; string drawingNo = _markingConfig.DrawingNo;
string hkmcNo = _markingConfig.HkmcNo; string hkmcNo = _markingConfig.HkmcNo;
string qrString = string.Format("{0};{1};{2};{3};", serial, drawingNo, hkmcNo, markedSerial); string qrString = string.Format("{0};{1};{2};{3};", serial, drawingNo, hkmcNo, markedSerial);
@ -1149,7 +1401,7 @@ namespace marking_gui
} }
// UI 스레드로 안전하게 컴백 // UI 스레드로 안전하게 컴백
Dispatcher.Invoke(() => await Dispatcher.InvokeAsync(async () =>
{ {
if (txtMarkedSerial != null) if (txtMarkedSerial != null)
{ {
@ -1188,7 +1440,7 @@ namespace marking_gui
LoggerService.Info($"[QR 검증 성공] S/N: {serial}, 각인번호: {markedSerial} -> DB 저장 진행"); LoggerService.Info($"[QR 검증 성공] S/N: {serial}, 각인번호: {markedSerial} -> DB 저장 진행");
// QR 검증 통과 시 최종 DB 저장 및 완료 처리 // QR 검증 통과 시 최종 DB 저장 및 완료 처리
SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, true, "OK", markedSerial); await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, true, "OK", markedSerial, bypassPrevSteps);
} }
else else
{ {
@ -1197,7 +1449,7 @@ namespace marking_gui
LoggerService.Warn($"[QR 검증 실패] S/N: {serial}, 각인번호: {markedSerial} -> DB NG 저장 진행"); LoggerService.Warn($"[QR 검증 실패] S/N: {serial}, 각인번호: {markedSerial} -> DB NG 저장 진행");
// 마킹 성공 후 QR 검증 불일치/취소 시 NG로 DB 저장 // 마킹 성공 후 QR 검증 불일치/취소 시 NG로 DB 저장
SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", markedSerial); await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", markedSerial, bypassPrevSteps);
} }
} }
else else
@ -1240,6 +1492,11 @@ namespace marking_gui
} }
}); });
} }
catch (OperationCanceledException)
{
LoggerService.Warn("[검사 중지] 마킹 단계에서 중지 요청이 수신되었습니다.");
// CancelInspectionSequenceAsync가 장비 복구 및 UI 정리를 담당
}
catch (Exception threadEx) catch (Exception threadEx)
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
@ -1265,25 +1522,28 @@ namespace marking_gui
txtSerial.IsEnabled = true; txtSerial.IsEnabled = true;
}); });
} }
}); }, token);
} }
else else
{ {
// 불합격인 경우 즉시 DB 저장 및 복구 // 불합격인 경우 즉시 DB 저장 및 복구
SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, isFinalPass, "NG", ""); await SaveAndFinalizeAsync(serial, finalVoltage, finalCurrent, finalResistance, isFinalPass, "NG", "", bypassPrevSteps);
} }
} }
} }
private async Task<(double Voltage, double Current, double Resistance)> ReadMeasurementFromDeviceAsync() private async Task<(double Voltage, double Current, double Resistance)> ReadMeasurementFromDeviceAsync(CancellationToken token = default)
{ {
try try
{ {
token.ThrowIfCancellationRequested();
// EquipmentMeasurementService 를 통해 실시간 계측값을 획득하는 즉시 GUI에 반영 // EquipmentMeasurementService 를 통해 실시간 계측값을 획득하는 즉시 GUI에 반영
var (voltage, current, resistance) = await _equipment.MeasureAsync((v, c, r) => var (voltage, current, resistance) = await _equipment.MeasureAsync((v, c, r) =>
{ {
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
if (token.IsCancellationRequested) return; // 취소됐으면 UI 갱신 스킵
if (v.HasValue) if (v.HasValue)
{ {
txtVoltage.Text = v.Value.ToString("F2"); txtVoltage.Text = v.Value.ToString("F2");
@ -1301,6 +1561,11 @@ namespace marking_gui
return (voltage, current, resistance); return (voltage, current, resistance);
} }
catch (OperationCanceledException)
{
// 취소 예외는 호출자(RunInspectionSequenceAsync)로 전파하여 일관된 중지 처리
throw;
}
catch (Exception ex) catch (Exception ex)
{ {
LoggerService.Error("[계측기] 측정 중 오류 발생", ex); LoggerService.Error("[계측기] 측정 중 오류 발생", ex);
@ -1321,7 +1586,7 @@ namespace marking_gui
} }
} }
private async void SaveAndFinalize(string serial, double voltage, double current, double resistance, bool isFinalPass, string result, string markedSerial) private async Task SaveAndFinalizeAsync(string serial, double voltage, double current, double resistance, bool isFinalPass, string result, string markedSerial, bool bypassPrevSteps = false)
{ {
// 종료 시간 기록 및 UI 갱신 // 종료 시간 기록 및 UI 갱신
_testEndTime = DateTime.Now; _testEndTime = DateTime.Now;
@ -1333,6 +1598,16 @@ namespace marking_gui
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
} }
// 강제검사(BYPASS) 시 또는 PCB 바코드가 없는 경우 시리얼 번호(serial)를 PCB 바코드로 대입
string pcbBarcodeToSave = (bypassPrevSteps || _currentProduct == null || string.IsNullOrWhiteSpace(_currentProduct.PcbBarcode))
? serial
: _currentProduct.PcbBarcode;
if (bypassPrevSteps && _currentProduct != null)
{
_currentProduct.PcbBarcode = serial;
}
try try
{ {
// 데이터베이스 저장 (비동기 스레드 격리로 UI 프리징 방지) // 데이터베이스 저장 (비동기 스레드 격리로 UI 프리징 방지)
@ -1355,7 +1630,7 @@ namespace marking_gui
_testStartTime, _testStartTime,
_testEndTime, _testEndTime,
markedSerial, markedSerial,
_currentProduct?.PcbBarcode, // Housing_Assembly 테이블에서 조회한 PCB 바코드 pcbBarcodeToSave, // 강제검사 시 시리얼 번호 사용
_markingConfig.DrawingNo, _markingConfig.DrawingNo,
_markingConfig.HkmcNo); _markingConfig.HkmcNo);
}); });
@ -1364,8 +1639,33 @@ namespace marking_gui
{ {
LoggerService.Info($"[DB 저장 완료] S/N: {serial}, 결과: {result}, V: {voltage:F2}, I: {current:F2}, R: {resistance:F2}"); LoggerService.Info($"[DB 저장 완료] S/N: {serial}, 결과: {result}, V: {voltage:F2}, I: {current:F2}, R: {resistance:F2}");
// 스캔 검증까지 포함한 최종 판정 결과 카드 갱신
if (isFinalPass && (result == "PASS" || result == "OK"))
{
txtFinalResult.Text = "OK";
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green text
if (borderVerdictCard != null)
{
borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(240, 253, 250)); // Light Green
borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green border
borderVerdictCard.BorderThickness = new Thickness(3);
}
txtSystemStatus.Text = "상태: 모든 공정 검사 및 QR 스캔 합격";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
}
else
{
txtFinalResult.Text = "NG";
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
if (borderVerdictCard != null)
{
borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(254, 242, 242)); // Light Red
borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red border
borderVerdictCard.BorderThickness = new Thickness(3);
}
txtSystemStatus.Text = (result == "NG") ? "상태: 각인 QR 검증 불합격 (NG)" : "상태: 검사 불합격";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
}
if (txtProgressStatus != null) if (txtProgressStatus != null)
{ {
@ -1469,7 +1769,12 @@ namespace marking_gui
_loginInfo = loginWindow.Result; _loginInfo = loginWindow.Result;
DisplayLoginInfo(); DisplayLoginInfo();
ResetUI(); ResetUI();
InitializeMarkingDevice();
// UI 스레드가 멈추지 않고 비동기로 마킹 장치 초기화 진행
Dispatcher.InvokeAsync(() =>
{
InitializeMarkingDevice();
}, System.Windows.Threading.DispatcherPriority.Background);
// 로그인 완료 후 보드 상시전원(Channel 2) 인가를 완전 비동기 백그라운드 스레드로 실행 (UI 프리징 100% 방지) // 로그인 완료 후 보드 상시전원(Channel 2) 인가를 완전 비동기 백그라운드 스레드로 실행 (UI 프리징 100% 방지)
_ = System.Threading.Tasks.Task.Run(async () => _ = System.Threading.Tasks.Task.Run(async () =>
@ -1575,16 +1880,23 @@ namespace marking_gui
_dbWasOffline = true; _dbWasOffline = true;
LoggerService.Warn("[DB 재연결 감시] DB 연결 끊김 감지"); LoggerService.Warn("[DB 재연결 감시] DB 연결 끊김 감지");
Dispatcher.Invoke(() => if (!_isInspectionRunning)
{ {
if (txtSystemStatus != null) Dispatcher.Invoke(() =>
{ {
txtSystemStatus.Text = "DB 연결 끊김 - 재연결 대기 중..."; SetStartButtonConnecting("⚠️ DB 연결 필요");
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
} if (txtSystemStatus != null)
if (txtNextSequence != null) {
txtNextSequence.Text = "연결 끊김"; txtSystemStatus.Text = "DB 연결 끊김 - 재연결 대기 중...";
}); txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11));
}
if (txtNextSequence != null)
txtNextSequence.Text = "연결 끊김";
if (btnRefreshSequence != null)
btnRefreshSequence.Visibility = Visibility.Visible;
});
}
} }
else if (isNowConnected && _dbWasOffline) else if (isNowConnected && _dbWasOffline)
{ {
@ -1592,17 +1904,27 @@ namespace marking_gui
_dbWasOffline = false; _dbWasOffline = false;
LoggerService.Info("[DB 재연결 감시] DB 재연결 감지 - 순번 자동 갱신"); LoggerService.Info("[DB 재연결 감시] DB 재연결 감지 - 순번 자동 갱신");
Dispatcher.Invoke(() => if (!_isInspectionRunning)
{ {
if (txtSystemStatus != null) Dispatcher.Invoke(() =>
{ {
txtSystemStatus.Text = "DB 재연결 완료 - 각인 순번 갱신 중..."; if (txtSystemStatus != null)
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); {
} txtSystemStatus.Text = "DB 재연결 완료 - 각인 순번 갱신 중...";
}); txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
}
if (btnRefreshSequence != null)
btnRefreshSequence.Visibility = Visibility.Collapsed;
});
// UI 스레드에서 순번 갱신 (비동기 안전) // UI 스레드에서 순번 갱신 (비동기 안전)
await Dispatcher.InvokeAsync(() => RefreshNextSequence()); await Dispatcher.InvokeAsync(() => RefreshNextSequence());
}
}
else if (isNowConnected && !_dbWasOffline && !_isInspectionRunning)
{
// DB 연결 유지 상태에서도 DB 레코드 직접 변경/삭제 시 각인 순번 실시간 반영 (조용히 UI 갱신)
await Dispatcher.InvokeAsync(() => RefreshNextSequenceSilently());
} }
} }
catch (Exception ex) catch (Exception ex)
@ -1663,6 +1985,14 @@ namespace marking_gui
// 도안 파일 선택 버튼 클릭 // 도안 파일 선택 버튼 클릭
private void btnSelectEzd_Click(object sender, RoutedEventArgs e) private void btnSelectEzd_Click(object sender, RoutedEventArgs e)
{ {
if (!CheckUserLoggedIn()) return;
if (_isInspectionRunning)
{
MessageBox.Show("현재 검사가 진행 중입니다.\n검사가 완료되거나 중지된 후 도안 파일을 변경하십시오.", "도안 변경 불가", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
try try
{ {
Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog(); Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog();
@ -1761,8 +2091,11 @@ namespace marking_gui
if (txtEzdFile != null) txtEzdFile.Text = fileName; if (txtEzdFile != null) txtEzdFile.Text = fileName;
string modelName = System.IO.Path.GetFileNameWithoutExtension(fileName); string modelName = System.IO.Path.GetFileNameWithoutExtension(fileName);
if (_loginInfo != null) _loginInfo.Model = modelName; if (_loginInfo == null || string.IsNullOrWhiteSpace(_loginInfo.Model) || _loginInfo.Model == "---")
if (txtModelInfo != null) txtModelInfo.Text = modelName; {
if (_loginInfo != null) _loginInfo.Model = modelName;
if (txtModelInfo != null) txtModelInfo.Text = modelName;
}
if (saveToConfig) if (saveToConfig)
{ {
@ -1806,6 +2139,14 @@ namespace marking_gui
return; return;
} }
if (txtSystemStatus != null && !_database.OfflineMode)
{
txtSystemStatus.Text = "DB 연결 시도 및 순번 조회 중...";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
}
bool sequenceFetched = false;
try try
{ {
int nextSeq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo); int nextSeq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo);
@ -1814,6 +2155,20 @@ namespace marking_gui
txtNextSequence.Text = string.Format("{0:D4} 차례", nextSeq); txtNextSequence.Text = string.Format("{0:D4} 차례", nextSeq);
} }
if (txtSystemStatus != null && !_database.OfflineMode)
{
txtSystemStatus.Text = "상태: DB 연결 완료 (정상)";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
}
// 정상 조회 성공 시 재연결 버튼 숨김
if (btnRefreshSequence != null)
{
btnRefreshSequence.Visibility = Visibility.Collapsed;
}
sequenceFetched = true;
if (nextSeq > 9999) if (nextSeq > 9999)
{ {
if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible; if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
@ -1832,13 +2187,122 @@ namespace marking_gui
} }
catch (Exception ex) catch (Exception ex)
{ {
_dbWasOffline = true;
LoggerService.Error("[UI RefreshSequence] 순번 갱신 중 에러", ex); LoggerService.Error("[UI RefreshSequence] 순번 갱신 중 에러", ex);
if (txtNextSequence != null) txtNextSequence.Text = "조회 실패"; if (txtNextSequence != null) txtNextSequence.Text = "조회 실패";
if (txtSystemStatus != null) if (txtSystemStatus != null)
{ {
txtSystemStatus.Text = $"각인 순번 조회 실패: {ex.Message}"; txtSystemStatus.Text = $"DB 연결 실패 / 순번 조회 에러: {ex.Message}";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
} }
// DB 연결/조회 실패 시 재연결 버튼 표기
if (btnRefreshSequence != null)
{
btnRefreshSequence.Visibility = Visibility.Visible;
}
}
finally
{
if (sequenceFetched)
{
_isInitialLoading = false;
_dbWasOffline = false;
SetStartButtonReady(); // 조회가 무사히 성공했을 때만 스타트 버튼 활성화!
}
else
{
_dbWasOffline = true;
SetStartButtonConnecting("⚠️ DB 연결 필요"); // 실패 시 계속 비활성화 유지
}
}
}
/// <summary>
/// DB 레코드가 수동으로 삭제/수정되었을 때 UI 상태창을 깜빡이지 않고 화면 각인순번만 조용히 실시간 자동 동기화
/// </summary>
private async void RefreshNextSequenceSilently()
{
if (_isInspectionRunning || _loginInfo == null || string.IsNullOrEmpty(_loginInfo.LotNo))
return;
try
{
int nextSeq = await GetNextMarkingSequenceAsync(_loginInfo.LotNo);
if (txtNextSequence != null)
{
txtNextSequence.Text = string.Format("{0:D4} 차례", nextSeq);
}
}
catch { }
}
private async void btnRefreshSequence_Click(object sender, RoutedEventArgs e)
{
SetStartButtonConnecting();
if (btnRefreshSequence != null)
{
btnRefreshSequence.IsEnabled = false;
btnRefreshSequence.Content = "⏳ 연결 시도 중...";
btnRefreshSequence.UpdateLayout();
// UI 렌더링 강제 갱신으로 작업자 피드백 즉시 전파
Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Render);
}
if (txtSystemStatus != null)
{
txtSystemStatus.Text = "DB 연결 시도 중...";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
}
if (txtNextSequence != null)
{
txtNextSequence.Text = "조회 중...";
}
try
{
string dbErrMsg = string.Empty;
bool isConnected = await Task.Run(() => _database.CheckConnection(out dbErrMsg));
if (isConnected)
{
_dbWasOffline = false;
if (btnRefreshSequence != null) btnRefreshSequence.Visibility = Visibility.Collapsed;
RefreshNextSequence();
}
else
{
_dbWasOffline = true;
if (txtNextSequence != null) txtNextSequence.Text = "연결 실패";
if (btnRefreshSequence != null) btnRefreshSequence.Visibility = Visibility.Visible;
if (txtSystemStatus != null)
{
txtSystemStatus.Text = string.IsNullOrWhiteSpace(dbErrMsg)
? "DB 연결 실패 - 네트워크 및 DB 설정 확인 필요"
: $"DB 연결 실패: {dbErrMsg}";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
}
}
}
catch (Exception ex)
{
if (txtNextSequence != null) txtNextSequence.Text = "오류 발생";
if (btnRefreshSequence != null) btnRefreshSequence.Visibility = Visibility.Visible;
if (txtSystemStatus != null)
{
txtSystemStatus.Text = $"DB 재연결 오류: {ex.Message}";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
}
}
finally
{
if (btnRefreshSequence != null)
{
btnRefreshSequence.IsEnabled = true;
btnRefreshSequence.Content = "🔄 DB 재연결";
}
SetStartButtonReady();
} }
} }
} }

14
marking_gui/Manual/.obsidian/workspace.json

@ -13,12 +13,12 @@
"state": { "state": {
"type": "markdown", "type": "markdown",
"state": { "state": {
"file": "07_시험_프로세스.md", "file": "01_시스템_개요.md",
"mode": "source", "mode": "source",
"source": false "source": false
}, },
"icon": "lucide-file", "icon": "lucide-file",
"title": "07_시험_프로세스" "title": "01_시스템_개요"
} }
} }
] ]
@ -185,26 +185,26 @@
}, },
"active": "b98d55f22f4d8058", "active": "b98d55f22f4d8058",
"lastOpenFiles": [ "lastOpenFiles": [
"02_초기_설정.md",
"03_메인_화면.md",
"07_시험_프로세스.md",
"사용자_메뉴얼.md",
"01_시스템_개요.md",
"08_로그_관리.md", "08_로그_관리.md",
"06_데이터_조회.md", "06_데이터_조회.md",
"07_시험_프로세스.md",
"09_오류_대응.md", "09_오류_대응.md",
"사용자_메뉴얼.md",
"05_시험_설정.md", "05_시험_설정.md",
"Pasted image 20260728100910.png", "Pasted image 20260728100910.png",
"Pasted image 20260728100846.png", "Pasted image 20260728100846.png",
"04_통신_설정.md", "04_통신_설정.md",
"Pasted image 20260728100828.png", "Pasted image 20260728100828.png",
"Pasted image 20260728100748.png", "Pasted image 20260728100748.png",
"03_메인_화면.md",
"Pasted image 20260728100445.png", "Pasted image 20260728100445.png",
"Pasted image 20260728100413.png", "Pasted image 20260728100413.png",
"Pasted image 20260728100404.png", "Pasted image 20260728100404.png",
"Pasted image 20260728100354.png", "Pasted image 20260728100354.png",
"Pasted image 20260728100343.png", "Pasted image 20260728100343.png",
"Pasted image 20260728100308.png", "Pasted image 20260728100308.png",
"02_초기_설정.md",
"01_시스템_개요.md",
"Manual.docx", "Manual.docx",
"build_docx.py", "build_docx.py",
"Manual_for_pdf.html" "Manual_for_pdf.html"

2
marking_gui/Manual/02_초기_설정.md

@ -52,7 +52,7 @@
현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다. 현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다.
```ini ```ini
[Settings] [Settings]
EzdFolderPath=c:\EzCad\EasyCfg ; EZD 도안 파일 기본 탐색/저장 폴더 경로 EzdFolderPath=c:\EzCad ; EZD 도안 파일 기본 탐색/저장 폴더 경로
test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false) test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false)
[Equipment] [Equipment]

10
marking_gui/Manual/03_메인_화면.md

@ -45,9 +45,9 @@
![[Pasted image 20260728100354.png]] ![[Pasted image 20260728100354.png]]
### 3.2.3 [카드 3] 측정 데이터 수치 카드 (우측 상단) ### 3.2.3 [카드 3] 측정 데이터 수치 카드 (우측 상단)
실제 계측기를 통해 수집된 3대 전기 파라미터 수치와 설정된 규격 범위를 표시하는 3열 구조의 카드입니다. 실제 계측기를 통해 수집된 3대 전기 파라미터 수치와 설정된 규격 범위를 표시하는 3열 구조의 카드입니다.
* **측정 전압 (Voltage)**: DMM 계측 장비가 측정한 실측 전압 수치 (**V** 단위, 기본 기준범위: `0.0V ~ 0.5V`) * **측정 전압 (Voltage)**: DMM 계측 장비가 측정한 실측 전압 수치 (**V** 단위, 기본 기준범위: `1.00V ~ 6.00V`)
* **측정 전류 (Current)**: Power Supply 전원공급 장치가 측정한 소모 전류 수치 (**mA** 단위, 기본 기준범위: `8.00mA ~ 10.00mA`) * **측정 전류 (Current)**: Power Supply 전원공급 장치가 측정한 소모 전류 수치 (**mA** 단위, 기본 기준범위: `5.00mA ~ 10.00mA`)
* **측정 저항 (Resistance)**: DMM 측정 장비가 측정한 전기적 저항 수치 (**kΩ** 단위, 기본 기준범위: `0.400kΩ ~ 0.800kΩ`) * **측정 저항 (Resistance)**: DMM 측정 장비가 측정한 전기적 저항 수치 (**kΩ** 단위, 기본 기준범위: `8.000kΩ ~ 13.000kΩ`)
* *알림: 각 수치 카드 하단에는 설정 파일(`Setting.ini`)에서 지정한 임계 규격 범위가 표시되며, 실측값이 범위 내에 들어오면 카드 테두리가 **초록색(PASS)**으로, 범위를 벗어나면 **빨간색(FAIL)**으로 자동 전환됩니다.* * *알림: 각 수치 카드 하단에는 설정 파일(`Setting.ini`)에서 지정한 임계 규격 범위가 표시되며, 실측값이 범위 내에 들어오면 카드 테두리가 **초록색(PASS)**으로, 범위를 벗어나면 **빨간색(FAIL)**으로 자동 전환됩니다.*
![[Pasted image 20260728100404.png]] ![[Pasted image 20260728100404.png]]
### 3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단) ### 3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)
@ -56,7 +56,7 @@
* **세부 정보 그리드**: * **세부 정보 그리드**:
- **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출 - **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- **일련번호 정보**: `IC S/N`, **`Marking QR :`** (마킹 생성 QR 값), **`Scan QR :`** (스캐너 실측 QR 값), `각인 번호` 4개 항목을 수직 배열로 표출 - **일련번호 정보**: `IC S/N`, **`Marking QR :`** (마킹 생성 QR 값), **`Scan QR :`** (스캐너 실측 QR 값), `각인 번호` 4개 항목을 수직 배열로 표출
- **최종 판정결과**: 최종 **PASS/OK** (초록색) / **FAIL/NG** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **NG**로 자동 반영됨) - **최종 판정결과**: 최종 **`OK`** (초록색) / **`NG`** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **`NG`**로 자동 반영됨)
- **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출 - **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
![[Pasted image 20260728100413.png]] ![[Pasted image 20260728100413.png]]
--- ---
@ -69,7 +69,7 @@
* **포트 설정**: 센서 통신 보드의 시리얼 COM 포트 및 채널 설정 창을 호출합니다. * **포트 설정**: 센서 통신 보드의 시리얼 COM 포트 및 채널 설정 창을 호출합니다.
* **강제 검사 (스킵)**: 이전 공정 이력이 없어도 검증을 건너뛰고 계측 및 마킹을 수행합니다. * **강제 검사 (스킵)**: 이전 공정 이력이 없어도 검증을 건너뛰고 계측 및 마킹을 수행합니다.
* **테스트 마킹**: 계측 과정 없이 레이저 각인 장비 동작을 테스트합니다. * **테스트 마킹**: 계측 과정 없이 레이저 각인 장비 동작을 테스트합니다.
* *참고: `강제 검사 (스킵)` `테스트 마킹` 버튼은 설정 파일(`Setting.ini`)에서 `TestMode=true`로 활성화된 경우에만 화면에 노출됩니다.* * *참고: `강제 검사 (스킵)` 버튼은 설정 파일(`Setting.ini`)에서 `test=true`로 활성화된 경우에만 화면에 노출됩니다.*
![[Pasted image 20260728100445.png]] ![[Pasted image 20260728100445.png]]

14
marking_gui/Manual/07_시험_프로세스.md

@ -22,8 +22,8 @@ graph TD
H --> I[12~14단계: 전압 / 저항 / 전류 실측] H --> I[12~14단계: 전압 / 저항 / 전류 실측]
I --> J[15단계: 테스트 주전원 안전 차단] I --> J[15단계: 테스트 주전원 안전 차단]
J --> K{최종 규격 비교 판정} J --> K{최종 규격 비교 판정}
K -- 1개 이상 이탈 FAIL --> L[대형 FAIL 표시 & 마킹 제외 & DB 불합격 적재] K -- 1개 이상 이탈 NG --> L[대형 NG 표시 & 마킹 제외 & DB NG 적재]
K -- 모두 통과 PASS --> M[대형 PASS 표시 & DB 합격 적재 & LMC1 레이저 QR 각인] K -- 모두 통과 OK --> M[대형 OK 표시 & DB OK 적재 & LMC1 레이저 QR 각인]
L --> Z L --> Z
M --> Z M --> Z
``` ```
@ -62,11 +62,11 @@ graph TD
계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다. 계측이 완료되면 측정된 전압(V), 저항(kΩ), 전류(mA) 값을 설정한 기준 규격값(SPEC)과 비교하여 최종 판정을 내립니다.
1. **기준 규격을 벗어난 경우 (FAIL)**: 1. **기준 규격을 벗어난 경우 (NG)**:
* 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(FAIL) 판정이 내려집니다. * 전압, 전류, 저항 중 하나라도 기준을 벗어나면 최종 불합격(**`NG`**) 판정이 내려집니다.
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다. * 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, `NG` 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (PASS)**: 2. **모든 항목이 합격인 경우 (OK)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다. * 모든 수치가 기준치 내에 들어오면 최종 합격(**`OK`**) 판정이 내려집니다.
* 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다. * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
* 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다. * 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.

2
marking_gui/Manual/08_로그_관리.md

@ -10,7 +10,7 @@
### 8.1.1 로그 파일 보관 방법 ### 8.1.1 로그 파일 보관 방법
* **보관 폴더**: 프로그램 실행 파일 경로의 하위 **`Logs`** 폴더에 저장됩니다. * **보관 폴더**: 프로그램 설치 경로(`C:\marking\`) 하위 **`logs/`** 폴더에 저장됩니다. (예: `C:\marking\logs\2026-08-05.log`)
* **파일명 규칙**: 날짜별로 파일이 자동으로 생성되어 보관됩니다. * **파일명 규칙**: 날짜별로 파일이 자동으로 생성되어 보관됩니다.
* **로그 중요도 분류**: * **로그 중요도 분류**:
* **정보 (INFO)**: 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록. * **정보 (INFO)**: 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록.

6
marking_gui/Manual/09_오류_대응.md

@ -31,7 +31,7 @@
### 9.1.4 [오류 4] "레이저 각인 작업 전송 실패" ### 9.1.4 [오류 4] "레이저 각인 작업 전송 실패"
* **현상**: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함. * **현상**: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함.
* **원인 및 해결**: * **원인 및 해결**:
1. **도면 파일 점검**: `시험 설정` 화면에서 세팅해 놓은 작업 도면 파일이 실제 해당 경로에 존재하는지 점검하십시오. 1. **도면 파일 점검**: 메인 화면의 `파일 선택` 버튼을 통해 선택한 작업 도면 파일(.ezd)이 실제 해당 경로에 존재하는지 점검하십시오.
2. **마킹기 전원/연결 점검**: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오. 2. **마킹기 전원/연결 점검**: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오.
### 9.1.5 [오류 5] "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오." ### 9.1.5 [오류 5] "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오."
@ -45,8 +45,8 @@
### 9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오." ### 9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오."
* **현상**: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음. * **현상**: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.
* **원인 및 해결**: * **원인 및 해결**:
1. **네트워크/DB 서버 점검**: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. `Database.ini` 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오. 1. **네트워크/DB 서버 점검**: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(3초)에 의해 경고가 표출됩니다. `Database.ini` 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
2. **계정 정보 확인**: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 `LoginOptions.json` 계정 목록에 올바르게 등록되어 있는지 재확인하십시오. 2. **계정 정보 확인**: 입력한 사용자 ID 및 비밀번호가 MSSQL `Users` 테이블에 활성 상태(`IsActive=1`)로 등록되어 있는지 확인하십시오. (DB 접속 실패 시에도 `LoginOptions.json` 파일의 `loginAccounts` 배열에 등록된 계정으로 1차 인증을 진행할 수 있으므로, 필요시 해당 파일의 계정 설정도 함께 점검하십시오.)
--- ---

22
marking_gui/Manual/Manual_Merged.md

@ -400,9 +400,9 @@ a:hover {
3. **프로그램 실행 및 로그인** 3. **프로그램 실행 및 로그인**
- 바탕화면의 `Marking System` (`marking_gui`) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.) - 바탕화면의 `Marking System` (`marking_gui`) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.)
4. **통신 상태 확인 및 채널 설정** 4. **통신 상태 확인 및 채널 설정**
- 우측 상단의 `통신 설정` 메뉴에서 센서가 올바른 포트와 채널에 연결되었는지 확인합니다. - 화면 **하단 바**의 `포트 설정` 버튼을 클릭하여 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.
5. **검사 규격값 검증** 5. **검사 규격값 검증**
- 우측 상단의 `시험 설정` 메뉴를 열고, 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다. - 화면 **하단 바**의 `파라미터 설정` 버튼을 클릭하여 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.
6. **성능 검사 및 마킹 시작** 6. **성능 검사 및 마킹 시작**
- 안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 `검사 시작 (START)` 버튼을 눌러 계측 및 마킹을 수행합니다. - 안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 `검사 시작 (START)` 버튼을 눌러 계측 및 마킹을 수행합니다.
@ -530,7 +530,7 @@ a:hover {
현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다. 현장 네트워크 IP, 도안 파일 경로 및 검사 규격(임계치) 변경 시 수정하는 주요 설정 항목입니다.
```ini ```ini
[Settings] [Settings]
EzdFolderPath=c:\EzCad\EasyCfg ; EZD 도안 파일 기본 탐색/저장 폴더 경로 EzdFolderPath=c:\EzCad ; EZD 도안 파일 기본 탐색/저장 폴더 경로
test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false) test=false ; 테스트 마킹 버튼 및 개발 모드 노출 여부 (true/false)
[Equipment] [Equipment]
@ -647,9 +647,9 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<img src="./Pasted image 20260728100354.png" alt="Pasted image 20260728100354.png" class="img-screenshot" /> <img src="./Pasted image 20260728100354.png" alt="Pasted image 20260728100354.png" class="img-screenshot" />
### 3.2.3 [카드 3] 측정 데이터 수치 카드 (우측 상단) ### 3.2.3 [카드 3] 측정 데이터 수치 카드 (우측 상단)
실제 계측기를 통해 수집된 3대 전기 파라미터 수치와 설정된 규격 범위를 표시하는 3열 구조의 카드입니다. 실제 계측기를 통해 수집된 3대 전기 파라미터 수치와 설정된 규격 범위를 표시하는 3열 구조의 카드입니다.
* **측정 전압 (Voltage)**: DMM 계측 장비가 측정한 실측 전압 수치 (**V** 단위, 기본 기준범위: `0.0V ~ 0.5V`) * **측정 전압 (Voltage)**: DMM 계측 장비가 측정한 실측 전압 수치 (**V** 단위, 기본 기준범위: `1.00V ~ 6.00V`)
* **측정 전류 (Current)**: Power Supply 전원공급 장치가 측정한 소모 전류 수치 (**mA** 단위, 기본 기준범위: `8.00mA ~ 10.00mA`) * **측정 전류 (Current)**: Power Supply 전원공급 장치가 측정한 소모 전류 수치 (**mA** 단위, 기본 기준범위: `5.00mA ~ 10.00mA`)
* **측정 저항 (Resistance)**: DMM 측정 장비가 측정한 전기적 저항 수치 (**kΩ** 단위, 기본 기준범위: `0.400kΩ ~ 0.800kΩ`) * **측정 저항 (Resistance)**: DMM 측정 장비가 측정한 전기적 저항 수치 (**kΩ** 단위, 기본 기준범위: `8.000kΩ ~ 13.000kΩ`)
* *알림: 각 수치 카드 하단에는 설정 파일(`Setting.ini`)에서 지정한 임계 규격 범위가 표시되며, 실측값이 범위 내에 들어오면 카드 테두리가 **초록색(PASS)**으로, 범위를 벗어나면 **빨간색(FAIL)**으로 자동 전환됩니다.* * *알림: 각 수치 카드 하단에는 설정 파일(`Setting.ini`)에서 지정한 임계 규격 범위가 표시되며, 실측값이 범위 내에 들어오면 카드 테두리가 **초록색(PASS)**으로, 범위를 벗어나면 **빨간색(FAIL)**으로 자동 전환됩니다.*
<img src="./Pasted image 20260728100404.png" alt="Pasted image 20260728100404.png" class="img-screenshot" /> <img src="./Pasted image 20260728100404.png" alt="Pasted image 20260728100404.png" class="img-screenshot" />
### 3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단) ### 3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)
@ -671,7 +671,7 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
* **포트 설정**: 센서 통신 보드의 시리얼 COM 포트 및 채널 설정 창을 호출합니다. * **포트 설정**: 센서 통신 보드의 시리얼 COM 포트 및 채널 설정 창을 호출합니다.
* **강제 검사 (스킵)**: 이전 공정 이력이 없어도 검증을 건너뛰고 계측 및 마킹을 수행합니다. * **강제 검사 (스킵)**: 이전 공정 이력이 없어도 검증을 건너뛰고 계측 및 마킹을 수행합니다.
* **테스트 마킹**: 계측 과정 없이 레이저 각인 장비 동작을 테스트합니다. * **테스트 마킹**: 계측 과정 없이 레이저 각인 장비 동작을 테스트합니다.
* *참고: `강제 검사 (스킵)` `테스트 마킹` 버튼은 설정 파일(`Setting.ini`)에서 `TestMode=true`로 활성화된 경우에만 화면에 노출됩니다.* * *참고: `강제 검사 (스킵)` 버튼은 설정 파일(`Setting.ini`)에서 `test=true`로 활성화된 경우에만 화면에 노출됩니다.*
<img src="./Pasted image 20260728100445.png" alt="Pasted image 20260728100445.png" class="img-screenshot" /> <img src="./Pasted image 20260728100445.png" alt="Pasted image 20260728100445.png" class="img-screenshot" />
@ -918,7 +918,7 @@ graph TD
### 8.1.1 로그 파일 보관 방법 ### 8.1.1 로그 파일 보관 방법
* **보관 폴더**: 프로그램 실행 파일 경로의 하위 **`Logs`** 폴더에 저장됩니다. * **보관 폴더**: 프로그램 설치 경로(`C:\marking\`) 하위 **`logs/`** 폴더에 저장됩니다. (예: `C:\marking\logs\2026-08-05.log`)
* **파일명 규칙**: 날짜별로 파일이 자동으로 생성되어 보관됩니다. * **파일명 규칙**: 날짜별로 파일이 자동으로 생성되어 보관됩니다.
* **로그 중요도 분류**: * **로그 중요도 분류**:
* **정보 (INFO)**: 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록. * **정보 (INFO)**: 정상적인 검사 프로세스 시작, 계측기 연결 성공, 레이저 인쇄 성공 및 데이터베이스 저장 완료 기록.
@ -979,7 +979,7 @@ graph TD
### 9.1.4 [오류 4] "레이저 각인 작업 전송 실패" ### 9.1.4 [오류 4] "레이저 각인 작업 전송 실패"
* **현상**: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함. * **현상**: 성능 검사는 합격(PASS)했으나, 레이저 장비로의 데이터 전송 단계에서 경고 에러가 발생함.
* **원인 및 해결**: * **원인 및 해결**:
1. **도면 파일 점검**: `시험 설정` 화면에서 세팅해 놓은 작업 도면 파일이 실제 해당 경로에 존재하는지 점검하십시오. 1. **도면 파일 점검**: 메인 화면의 `파일 선택` 버튼을 통해 선택한 작업 도면 파일(.ezd)이 실제 해당 경로에 존재하는지 점검하십시오.
2. **마킹기 전원/연결 점검**: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오. 2. **마킹기 전원/연결 점검**: 레이저 마킹기의 전원이 켜져 있는지 확인하고, 제어 PC와 마킹기 간의 연결 케이블 상태를 확인하십시오.
### 9.1.5 [오류 5] "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오." ### 9.1.5 [오류 5] "각인 순번이 9999를 초과했습니다. 다음 Lot No로 변경하십시오."
@ -993,8 +993,8 @@ graph TD
### 9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오." ### 9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오."
* **현상**: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음. * **현상**: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.
* **원인 및 해결**: * **원인 및 해결**:
1. **네트워크/DB 서버 점검**: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. `Database.ini` 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오. 1. **네트워크/DB 서버 점검**: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(3초)에 의해 경고가 표출됩니다. `Database.ini` 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
2. **계정 정보 확인**: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 `LoginOptions.json` 계정 목록에 올바르게 등록되어 있는지 재확인하십시오. 2. **계정 정보 확인**: 입력한 사용자 ID 및 비밀번호가 MSSQL `Users` 테이블에 활성 상태(`IsActive=1`)로 등록되어 있는지 확인하십시오. (DB 접속 실패 시에도 `LoginOptions.json` 파일의 `loginAccounts` 배열에 등록된 계정으로 1차 인증을 진행할 수 있으므로, 필요시 해당 파일의 계정 설정도 함께 점검하십시오.)
--- ---

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

@ -67,8 +67,8 @@
3. **프로그램 실행 및 로그인** 3. **프로그램 실행 및 로그인**
- 바탕화면의 `Marking System` (`marking_gui`) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.) - 바탕화면의 `Marking System` (`marking_gui`) 프로그램을 실행한 뒤, 작업자 계정 정보를 입력하여 로그인합니다. (데이터베이스 점검 중인 경우 설정 파일을 통해 오프라인 모드로의 진입이 가능합니다.)
4. **통신 상태 확인 및 채널 설정** 4. **통신 상태 확인 및 채널 설정**
- 우측 상단의 `통신 설정` 메뉴에서 센서가 올바른 포트와 채널에 연결되었는지 확인합니다. - 화면 **하단 바**의 `포트 설정` 버튼을 클릭하여 센서가 올바른 포트와 채널에 연결되었는지 확인합니다.
5. **검사 규격값 검증** 5. **검사 규격값 검증**
- 우측 상단의 `시험 설정` 메뉴를 열고, 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다. - 화면 **하단 바**의 `파라미터 설정` 버튼을 클릭하여 전압/전류/저항 검사 기준치와 각인 템플릿 도면 파일의 경로가 올바른지 점검합니다.
6. **성능 검사 및 마킹 시작** 6. **성능 검사 및 마킹 시작**
- 안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 `검사 시작 (START)` 버튼을 눌러 계측 및 마킹을 수행합니다. - 안착 지그에 제품을 바르게 장착한 뒤 메인 화면 하단의 `검사 시작 (START)` 버튼을 눌러 계측 및 마킹을 수행합니다.

40
marking_gui/PortSettingsWindow.xaml.cs

@ -217,28 +217,34 @@ namespace marking_gui
SerialBarcodeScanner tempScanner = null; SerialBarcodeScanner tempScanner = null;
try try
{ {
tempScanner = new SerialBarcodeScanner(tempSettings); try
} {
catch { } tempScanner = new SerialBarcodeScanner(tempSettings);
}
string testSampleQr = "TEST_PORT_SCAN;DRAWING_01;HKMC_99;2607299999;"; catch { }
var qrTestWin = new QrVerificationWindow(testSampleQr, tempScanner)
{
Owner = this,
Title = $"📷 [스캐너 포트 {scannerPort} / {scannerBaud} Baud] 연동 테스트"
};
bool? testResult = qrTestWin.ShowDialog(); string testSampleQr = "TEST_PORT_SCAN;DRAWING_01;HKMC_99;2607299999;";
var qrTestWin = new QrVerificationWindow(testSampleQr, tempScanner)
{
Owner = this,
Title = $"📷 [스캐너 포트 {scannerPort} / {scannerBaud} Baud] 연동 테스트"
};
try { tempScanner?.Dispose(); } catch { } bool? testResult = qrTestWin.ShowDialog();
if (testResult == true) if (testResult == true)
{ {
MessageBox.Show($"스캐너 (포트: {scannerPort}) 통신 및 QR 대조 검증 성공!\n\n현재 선택한 포트로 스캐너가 정상 작동합니다.", "스캔 테스트 성공", MessageBoxButton.OK, MessageBoxImage.Information); MessageBox.Show($"스캐너 (포트: {scannerPort}) 통신 및 QR 대조 검증 성공!\n\n현재 선택한 포트로 스캐너가 정상 작동합니다.", "스캔 테스트 성공", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show($"스캔 테스트가 중단되었거나 취소되었습니다.", "스캔 테스트 종료", MessageBoxButton.OK, MessageBoxImage.Information);
}
} }
else finally
{ {
MessageBox.Show($"스캔 테스트가 중단되었거나 취소되었습니다.", "스캔 테스트 종료", MessageBoxButton.OK, MessageBoxImage.Information); try { tempScanner?.Stop(); } catch { }
try { tempScanner?.Dispose(); } catch { }
} }
} }

19
marking_gui/QrVerificationWindow.xaml.cs

@ -51,8 +51,11 @@ namespace marking_gui
private void SerialScanner_ScanReceived(object? sender, BarcodeScanReceivedEventArgs e) private void SerialScanner_ScanReceived(object? sender, BarcodeScanReceivedEventArgs e)
{ {
if (_isClosing || !IsLoaded || !IsVisible) return;
Dispatcher.BeginInvoke(new Action(async () => Dispatcher.BeginInvoke(new Action(async () =>
{ {
if (_isClosing || !IsLoaded || !IsVisible) return;
if (!string.IsNullOrWhiteSpace(e.Text)) if (!string.IsNullOrWhiteSpace(e.Text))
{ {
_serialScanner?.ConfirmPort(e.PortName); _serialScanner?.ConfirmPort(e.PortName);
@ -203,14 +206,6 @@ namespace marking_gui
txtStatusMessage.Text = "QR 코드 검증 성공"; txtStatusMessage.Text = "QR 코드 검증 성공";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green
} }
// 스캔값이 합격이면 딜레이 없이 즉시 팝업 닫기
if (!_isClosing)
{
_isClosing = true;
DialogResult = true;
Close();
}
} }
else else
{ {
@ -244,6 +239,14 @@ namespace marking_gui
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
} }
} }
// 합격이든 불합격이든 스캔 완료 즉시 팝업 닫기
if (!_isClosing)
{
_isClosing = true;
DialogResult = _isPassed;
Close();
}
} }
private static string NormalizeQrFormat(string input) private static string NormalizeQrFormat(string input)

61
marking_gui/Services/DatabaseService.cs

@ -92,7 +92,8 @@ namespace marking_gui.Services
!string.IsNullOrWhiteSpace(dbUser) && !string.IsNullOrWhiteSpace(dbUser) &&
!string.IsNullOrWhiteSpace(dbPassword)) !string.IsNullOrWhiteSpace(dbPassword))
{ {
_connectionString = $"Server={dbServer};Database={dbName};User Id={dbUser};Password={dbPassword};"; string cleanServer = dbServer.Trim().Replace(" ", "");
_connectionString = $"Server={cleanServer};Database={dbName};User Id={dbUser};Password={dbPassword};";
} }
else else
{ {
@ -120,13 +121,13 @@ namespace marking_gui.Services
_connectionString += "TrustServerCertificate=True;"; _connectionString += "TrustServerCertificate=True;";
} }
// DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 1초로 강제 조정하고 재시도 비활성화 // DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 3초로 조정
if (!string.IsNullOrWhiteSpace(_connectionString) && if (!string.IsNullOrWhiteSpace(_connectionString) &&
_connectionString.IndexOf("Connect Timeout", StringComparison.OrdinalIgnoreCase) < 0 && _connectionString.IndexOf("Connect Timeout", StringComparison.OrdinalIgnoreCase) < 0 &&
_connectionString.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0) _connectionString.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0)
{ {
if (!_connectionString.EndsWith(";")) _connectionString += ";"; if (!_connectionString.EndsWith(";")) _connectionString += ";";
_connectionString += "Connect Timeout=1;Connect Retry Count=0;"; _connectionString += "Connect Timeout=3;";
} }
} }
catch (Exception ex) catch (Exception ex)
@ -138,11 +139,20 @@ namespace marking_gui.Services
public bool CheckConnection() public bool CheckConnection()
{ {
return CheckConnection(out _);
}
public bool CheckConnection(out string errorMessage)
{
errorMessage = string.Empty;
if (_offlineMode) if (_offlineMode)
return true; return true;
if (string.IsNullOrWhiteSpace(_connectionString)) if (string.IsNullOrWhiteSpace(_connectionString))
{
errorMessage = "데이터베이스 연결 문자열이 설정되지 않았습니다. (Database.ini 파일 점검 필요)";
return false; return false;
}
try try
{ {
@ -152,8 +162,10 @@ namespace marking_gui.Services
return true; return true;
} }
} }
catch catch (Exception ex)
{ {
errorMessage = ex.Message;
System.Diagnostics.Debug.WriteLine("[DB CheckConnection] 연결 실패: " + ex.Message);
return false; return false;
} }
} }
@ -217,6 +229,7 @@ namespace marking_gui.Services
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine("[DB GetProduct] 이전 공정 결과 조회 중 에러: " + ex.Message); System.Diagnostics.Debug.WriteLine("[DB GetProduct] 이전 공정 결과 조회 중 에러: " + ex.Message);
throw;
} }
return null; return null;
@ -293,7 +306,7 @@ namespace marking_gui.Services
{ {
serial = serial.Substring(0, 30); serial = serial.Substring(0, 30);
} }
string safeResult = string.IsNullOrEmpty(result) ? "NG" : (result.Length > 5 ? result.Substring(0, 5) : result); string safeResult = (result == "PASS" || result == "OK") ? "OK" : "NG";
// DB 컬럼 길이 한계에 맞춰서 문자열 안전하게 자르기 (Truncate) // DB 컬럼 길이 한계에 맞춰서 문자열 안전하게 자르기 (Truncate)
string safeMaker = string.IsNullOrEmpty(maker) ? "" : (maker.Length > 10 ? maker.Substring(0, 10) : maker); string safeMaker = string.IsNullOrEmpty(maker) ? "" : (maker.Length > 10 ? maker.Substring(0, 10) : maker);
@ -332,6 +345,12 @@ namespace marking_gui.Services
return true; return true;
} }
string safePcbBarcode = string.IsNullOrWhiteSpace(pcbBarcode) ? serial : pcbBarcode.Trim();
if (safePcbBarcode.Length > 30)
{
safePcbBarcode = safePcbBarcode.Substring(0, 30);
}
// 실제 MSSQL DB 적재 실행 (동일 IC_SN 재검사 시 데이터가 덮어씌워지도록 MERGE 문 도입) // 실제 MSSQL DB 적재 실행 (동일 IC_SN 재검사 시 데이터가 덮어씌워지도록 MERGE 문 도입)
try try
{ {
@ -340,6 +359,7 @@ namespace marking_gui.Services
ON (target.[IC_SN] = source.IC_SN) ON (target.[IC_SN] = source.IC_SN)
WHEN MATCHED THEN WHEN MATCHED THEN
UPDATE SET UPDATE SET
[PCB_Barcode] = @PcbBarcode,
[Maker] = @Maker, [Maker] = @Maker,
[Model] = @Model, [Model] = @Model,
[Variant_1] = @Var1, [Variant_1] = @Var1,
@ -355,8 +375,8 @@ namespace marking_gui.Services
[Marking_QR] = @Qr, [Marking_QR] = @Qr,
[Result] = @Result [Result] = @Result
WHEN NOT MATCHED THEN WHEN NOT MATCHED THEN
INSERT ([IC_SN], [Marking_QR], [Maker], [Model], [Variant_1], [Variant_2], [Operator], [Production_Date], [Line], [Lot_No], [Jig_No], [PT_Vol_1], [PT_Current_1], [PT_Resistance_1], [Result]) INSERT ([IC_SN], [PCB_Barcode], [Marking_QR], [Maker], [Model], [Variant_1], [Variant_2], [Operator], [Production_Date], [Line], [Lot_No], [Jig_No], [PT_Vol_1], [PT_Current_1], [PT_Resistance_1], [Result])
VALUES (@Serial, @Qr, @Maker, @Model, @Var1, @Var2, @Operator, @ProdDate, @Line, @LotNo, @JigNo, @Vol1, @Cur1, @Res1, @Result);"; VALUES (@Serial, @PcbBarcode, @Qr, @Maker, @Model, @Var1, @Var2, @Operator, @ProdDate, @Line, @LotNo, @JigNo, @Vol1, @Cur1, @Res1, @Result);";
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString)) using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{ {
@ -364,6 +384,7 @@ namespace marking_gui.Services
{ {
cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초 cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
cmd.Parameters.AddWithValue("@Serial", serial); cmd.Parameters.AddWithValue("@Serial", serial);
cmd.Parameters.AddWithValue("@PcbBarcode", safePcbBarcode);
cmd.Parameters.AddWithValue("@Qr", markingQr); cmd.Parameters.AddWithValue("@Qr", markingQr);
cmd.Parameters.AddWithValue("@Maker", safeMaker); cmd.Parameters.AddWithValue("@Maker", safeMaker);
cmd.Parameters.AddWithValue("@Model", safeModel); cmd.Parameters.AddWithValue("@Model", safeModel);
@ -415,14 +436,24 @@ namespace marking_gui.Services
try try
{ {
// Marking_QR (형식: [IC_SN];[도면품번];[HKMC품번];[markedSerial];)에서 맨 뒤 세미콜론 직전의 markedSerial 마지막 4자리를 정수로 추출하여 MAX 산출 // SQL Server 2022 호환: Marking_QR에서 맨 뒤 4자리를 TRY_CAST로 안전하게 정수로 변환하여 MAX 순번 산출 (NOLOCK 적용)
string query = @" string query = @"
SELECT ISNULL(MAX(CAST(RIGHT(RTRIM(SUBSTRING([Marking_QR], 1, LEN([Marking_QR]) - 1)), 4) AS INT)), 0) SELECT ISNULL(MAX(
FROM [dbo].[Marking] TRY_CAST(
RIGHT(
RTRIM(
CASE
WHEN RTRIM([Marking_QR]) LIKE '%;' THEN LEFT(RTRIM([Marking_QR]), LEN(RTRIM([Marking_QR])) - 1)
ELSE RTRIM([Marking_QR])
END
), 4
) AS INT
)
), 0)
FROM [dbo].[Marking] WITH (NOLOCK)
WHERE [Lot_No] = @LotNo WHERE [Lot_No] = @LotNo
AND [Marking_QR] LIKE '%;%' AND [Marking_QR] IS NOT NULL
AND LEN([Marking_QR]) >= 5 AND LEN(RTRIM([Marking_QR])) >= 4;";
AND ISNUMERIC(RIGHT(RTRIM(SUBSTRING([Marking_QR], 1, LEN([Marking_QR]) - 1)), 4)) = 1";
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString)) using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{ {
@ -439,8 +470,8 @@ namespace marking_gui.Services
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine("[DB GetNextMarkingSequence] 에러: " + ex.Message); System.Diagnostics.Debug.WriteLine("[DB GetNextMarkingSequence] 에러 발생: " + ex.Message);
throw; // 예외를 상위로 전파하여 DB 장애 시 중복 순번 발급 방지 throw; // 예외 원인을 상위로 전파하여 UI 상태창에 구체적인 원인 출력
} }
} }
} }

37
marking_gui/Services/EquipmentMeasurementService.cs

@ -290,6 +290,43 @@ namespace marking_gui.Services
await SendCommandListAsync(client, _logoutCommand); await SendCommandListAsync(client, _logoutCommand);
} }
/// <summary>
/// 검사 중단(STOP) 또는 긴급 복구 시 PSU 출력을 안전하게 OFF 합니다.
/// MeasureAsync의 finally와 독립적으로 동작하여, 시퀀스 강제 취소 시에도
/// 파워서플라이 출력이 항상 OFF 상태가 되도록 보장합니다.
/// </summary>
public async Task ResetAsync()
{
if (_psu.IsDisabled)
{
LoggerService.Warn("[Equipment] ResetAsync 스킵: Power Supply 설정이 비활성화 상태입니다.");
return;
}
var client = GetPsuClient();
if (client == null)
{
LoggerService.Warn("[Equipment] ResetAsync 스킵: Power Supply 클라이언트를 생성할 수 없습니다.");
return;
}
try
{
string cleanupCmd = !string.IsNullOrWhiteSpace(_psu.CleanupCommand)
? _psu.CleanupCommand
: "OUTP OFF, (@1)";
LoggerService.Info($"[Equipment] ResetAsync: PSU 출력 OFF 강제 실행 ({cleanupCmd})");
await SendCommandListAsync(client, cleanupCmd);
LoggerService.Info("[Equipment] ResetAsync 완료: PSU 출력 OFF 확인됨");
}
catch (Exception ex)
{
LoggerService.Warn($"[Equipment] ResetAsync 실패 (PSU 통신 오류): {ex.Message}");
}
}
// ── 내부 측정 헬퍼 ────────────────────────────────────────────── // ── 내부 측정 헬퍼 ──────────────────────────────────────────────
private async Task<double> ReadChannelAsync( private async Task<double> ReadChannelAsync(
string label, string label,

73
marking_gui/Services/EzdMarkingService.cs

@ -7,13 +7,20 @@ namespace marking_gui.Services
{ {
public class EzdMarkingService : IMarkingService public class EzdMarkingService : IMarkingService
{ {
private readonly object _handleLock = new object();
private bool _isInitialized = false; private bool _isInitialized = false;
private IntPtr _markingHandle = IntPtr.Zero; // 마킹 장치 기기 핸들 포인터 private IntPtr _markingHandle = IntPtr.Zero; // 마킹 장치 기기 핸들 포인터
private string _ezdFolderPath = string.Empty; private string _ezdFolderPath = string.Empty;
private MarkingConfigService _config; private MarkingConfigService _config;
private string _lastInitErrorMessage = "초기화가 진행되지 않았습니다."; private string _lastInitErrorMessage = "초기화가 진행되지 않았습니다.";
public string LastInitErrorMessage => _lastInitErrorMessage; public string LastInitErrorMessage
{
get
{
lock (_handleLock) { return _lastInitErrorMessage; }
}
}
// ========================================== // ==========================================
// MarkEzdWrapper.dll P/Invoke 선언부 // MarkEzdWrapper.dll P/Invoke 선언부
@ -57,8 +64,10 @@ namespace marking_gui.Services
public bool Initialize(string configPath, IntPtr windowHandle) public bool Initialize(string configPath, IntPtr windowHandle)
{ {
try lock (_handleLock)
{ {
try
{
_lastConfigPath = configPath; _lastConfigPath = configPath;
if (windowHandle != IntPtr.Zero) _lastWindowHandle = windowHandle; if (windowHandle != IntPtr.Zero) _lastWindowHandle = windowHandle;
@ -140,12 +149,13 @@ namespace marking_gui.Services
System.Diagnostics.Debug.WriteLine("[EzdMarking] MarkEzdWrapper.dll 또는 하위 DLL(Lmc1.dll 등)을 로드할 수 없습니다. 실행 폴더에 복사했는지 확인하세요. 상세: " + ex.Message); System.Diagnostics.Debug.WriteLine("[EzdMarking] MarkEzdWrapper.dll 또는 하위 DLL(Lmc1.dll 등)을 로드할 수 없습니다. 실행 폴더에 복사했는지 확인하세요. 상세: " + ex.Message);
return false; return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
_isInitialized = false; _isInitialized = false;
_lastInitErrorMessage = "마킹 컨트롤 보드 연동 예외 발생: " + ex.Message; _lastInitErrorMessage = "마킹 컨트롤 보드 연동 예외 발생: " + ex.Message;
System.Diagnostics.Debug.WriteLine("[EzdMarking] 초기화 예외 발생: " + ex.Message); System.Diagnostics.Debug.WriteLine("[EzdMarking] 초기화 예외 발생: " + ex.Message);
return false; return false;
}
} }
} }
@ -230,11 +240,16 @@ namespace marking_gui.Services
string icSerial = string.IsNullOrWhiteSpace(request.IcSerial) ? request.Serial : request.IcSerial; string icSerial = string.IsNullOrWhiteSpace(request.IcSerial) ? request.Serial : request.IcSerial;
request.QrString = string.Format("{0};{1};{2};{3};", icSerial, request.DrawingNo, request.HkmcNo, request.Serial); request.QrString = string.Format("{0};{1};{2};{3};", icSerial, request.DrawingNo, request.HkmcNo, request.Serial);
// 2. 도안 내 변수 태그 치환 (사양서 4대 태그: S/N, DMC, 도면품번, HKMC품번) // 2. 도안 내 지정된 5개 변수 태그만 치환 (SERIAL, YYMMDD, DATAMATRIX, PARTNO, HKMCPO)
ChangeTextInEzd(_config.SerialTag, request.Serial); string seqOnly = (!string.IsNullOrEmpty(request.Serial) && request.Serial.Length >= 4)
ChangeTextInEzd(_config.QrCodeTag, request.QrString); ? request.Serial.Substring(request.Serial.Length - 4)
ChangeTextInEzd(_config.DrawingNoTag, request.DrawingNo); : request.Serial;
ChangeTextInEzd(_config.HkmcNoTag, request.HkmcNo);
ChangeTextInEzd(_config.SerialTag, seqOnly); // 1. SERIAL (4자리 순번)
ChangeTextInEzd(_config.LotNoTag, request.LotNo); // 2. YYMMDD (LotNo)
ChangeTextInEzd(_config.QrCodeTag, request.QrString); // 3. DATAMATRIX (QR코드)
ChangeTextInEzd(_config.DrawingNoTag, request.DrawingNo); // 4. PARTNO (도면품번)
ChangeTextInEzd(_config.HkmcNoTag, request.HkmcNo); // 5. HKMCPO (HKMC품번)
System.Diagnostics.Debug.WriteLine(string.Format( System.Diagnostics.Debug.WriteLine(string.Format(
"[EzdMarking] 도안 텍스트 치환 완료. Serial:{0}, LotNo:{1}, Product:{2}, Drawing:{3}, Hkmc:{4}, Qr:{5}", "[EzdMarking] 도안 텍스트 치환 완료. Serial:{0}, LotNo:{1}, Product:{2}, Drawing:{3}, Hkmc:{4}, Qr:{5}",
@ -329,21 +344,39 @@ namespace marking_gui.Services
} }
} }
public void Shutdown() public void StopMarking()
{ {
try try
{ {
if (_isInitialized && _markingHandle != IntPtr.Zero) if (_markingHandle != IntPtr.Zero)
{ {
Close(_markingHandle); System.Diagnostics.Debug.WriteLine("[EzdMarking] StopMarking 요청 처리 완료");
_markingHandle = IntPtr.Zero;
_isInitialized = false;
System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 컨트롤 디바이스 정상 종료 완료.");
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine("[EzdMarking] 디바이스 종료 중 예외: " + ex.Message); System.Diagnostics.Debug.WriteLine("[EzdMarking] StopMarking 중 예외 발생: " + ex.Message);
}
}
public void Shutdown()
{
lock (_handleLock)
{
try
{
if (_isInitialized && _markingHandle != IntPtr.Zero)
{
Close(_markingHandle);
_markingHandle = IntPtr.Zero;
_isInitialized = false;
System.Diagnostics.Debug.WriteLine("[EzdMarking] 마킹 컨트롤 디바이스 정상 종료 완료.");
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[EzdMarking] 디바이스 종료 중 예외: " + ex.Message);
}
} }
} }

5
marking_gui/Services/IMarkingService.cs

@ -28,5 +28,10 @@ namespace marking_gui.Services
/// 마킹 드라이버 및 리소스를 정상적으로 해제합니다. /// 마킹 드라이버 및 리소스를 정상적으로 해제합니다.
/// </summary> /// </summary>
void Shutdown(); void Shutdown();
/// <summary>
/// 진행 중인 마킹 작업을 즉시 중단하고 정상 상태로 리셋합니다.
/// </summary>
void StopMarking();
} }
} }

5
marking_gui/Services/LoginServicesStub.cs

@ -26,7 +26,8 @@ namespace Housing.Services
!string.IsNullOrWhiteSpace(Db_User) && !string.IsNullOrWhiteSpace(Db_User) &&
!string.IsNullOrWhiteSpace(Db_Password)) !string.IsNullOrWhiteSpace(Db_Password))
{ {
connStr = $"Server={Db_Server};Database={Db_Name};User Id={Db_User};Password={Db_Password};"; string cleanServer = Db_Server.Trim().Replace(" ", "");
connStr = $"Server={cleanServer};Database={Db_Name};User Id={Db_User};Password={Db_Password};";
} }
else else
{ {
@ -44,7 +45,7 @@ namespace Housing.Services
connStr.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0) connStr.IndexOf("Connection Timeout", StringComparison.OrdinalIgnoreCase) < 0)
{ {
if (!connStr.EndsWith(";")) connStr += ";"; if (!connStr.EndsWith(";")) connStr += ";";
connStr += "Connect Timeout=1;Connect Retry Count=0;"; connStr += "Connect Timeout=3;";
} }
return connStr; return connStr;

29
marking_gui/Services/MarkingConfigService.cs

@ -84,21 +84,24 @@ namespace marking_gui.Services
if (!found) if (!found)
{ {
// [Settings] 섹션 아래에 추가 // [Settings] 섹션 아래에 추가
bool sectionFound = false;
for (int i = 0; i < lines.Count; i++) for (int i = 0; i < lines.Count; i++)
{ {
if (lines[i].Trim().Equals("[Settings]", StringComparison.OrdinalIgnoreCase)) if (lines[i].Trim().Equals("[Settings]", StringComparison.OrdinalIgnoreCase))
{ {
lines.Insert(i + 1, "LastSelectedEzdFile=" + filePath); lines.Insert(i + 1, "LastSelectedEzdFile=" + filePath);
found = true; sectionFound = true;
break; break;
} }
} }
if (!sectionFound)
{
lines.Insert(0, "[Settings]");
lines.Insert(1, "LastSelectedEzdFile=" + filePath);
}
} }
if (found) File.WriteAllLines(iniPath, lines);
{
File.WriteAllLines(iniPath, lines);
}
} }
} }
catch (Exception ex) catch (Exception ex)
@ -261,7 +264,21 @@ namespace marking_gui.Services
} }
if (!found) if (!found)
{ {
lines.Add(string.Format("{0}={1}", key, sequence)); bool sectionFound = false;
for (int i = 0; i < lines.Count; i++)
{
if (lines[i].Trim().Equals("[Settings]", StringComparison.OrdinalIgnoreCase))
{
lines.Insert(i + 1, string.Format("{0}={1}", key, sequence));
sectionFound = true;
break;
}
}
if (!sectionFound)
{
lines.Insert(0, "[Settings]");
lines.Insert(1, string.Format("{0}={1}", key, sequence));
}
} }
File.WriteAllLines(iniPath, lines); File.WriteAllLines(iniPath, lines);
} }

3
marking_gui/Setting.ini

@ -26,9 +26,6 @@ SerialTag=SERIAL
; 작업 년월일 (Lot No) 치환 태그 ; 작업 년월일 (Lot No) 치환 태그
LotNoTag=YYMMDD LotNoTag=YYMMDD
; 제품 모델명 (Product Name) 치환 태그
ProductTag=PRODUCT
; DMC 치환 태그 ; DMC 치환 태그
QrCodeTag=DATAMATRIX QrCodeTag=DATAMATRIX

27
marking_gui/SettingsWindow.xaml.cs

@ -28,18 +28,33 @@ namespace marking_gui
private void btnSave_Click(object sender, RoutedEventArgs e) private void btnSave_Click(object sender, RoutedEventArgs e)
{ {
double minV, maxV, minC, maxC, minR, maxR; double minV, maxV, minC, maxC, minR, maxR;
var style = System.Globalization.NumberStyles.Number;
var culture = System.Globalization.CultureInfo.InvariantCulture;
if (!double.TryParse(txtMinVoltage.Text, out minV) || string sMinV = txtMinVoltage.Text.Replace(',', '.');
!double.TryParse(txtMaxVoltage.Text, out maxV) || string sMaxV = txtMaxVoltage.Text.Replace(',', '.');
!double.TryParse(txtMinCurrent.Text, out minC) || string sMinC = txtMinCurrent.Text.Replace(',', '.');
!double.TryParse(txtMaxCurrent.Text, out maxC) || string sMaxC = txtMaxCurrent.Text.Replace(',', '.');
!double.TryParse(txtMinResistance.Text, out minR) || string sMinR = txtMinResistance.Text.Replace(',', '.');
!double.TryParse(txtMaxResistance.Text, out maxR)) string sMaxR = txtMaxResistance.Text.Replace(',', '.');
if (!double.TryParse(sMinV, style, culture, out minV) ||
!double.TryParse(sMaxV, style, culture, out maxV) ||
!double.TryParse(sMinC, style, culture, out minC) ||
!double.TryParse(sMaxC, style, culture, out maxC) ||
!double.TryParse(sMinR, style, culture, out minR) ||
!double.TryParse(sMaxR, style, culture, out maxR))
{ {
MessageBox.Show("기준 범위 설정 값은 올바른 숫자 형식이어야 합니다.", "설정 오류", MessageBoxButton.OK, MessageBoxImage.Warning); MessageBox.Show("기준 범위 설정 값은 올바른 숫자 형식이어야 합니다.", "설정 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
return; return;
} }
if (minV >= maxV || minC >= maxC || minR >= maxR)
{
MessageBox.Show("최솟값은 최댓값보다 엄격히 작아야 합니다.\n\n입력한 최소/최대 범위를 다시 확인해 주십시오.", "설정 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
MeasurementLimits tempLimits = new MeasurementLimits(minV, maxV, minC, maxC, minR, maxR); MeasurementLimits tempLimits = new MeasurementLimits(minV, maxV, minC, maxC, minR, maxR);
string errorMessage; string errorMessage;
if (!tempLimits.Validate(out errorMessage)) if (!tempLimits.Validate(out errorMessage))

7
marking_gui/StartupLoginWindow.xaml

@ -19,6 +19,11 @@
SnapsToDevicePixels="True"> SnapsToDevicePixels="True">
<Window.Resources> <Window.Resources>
<Style TargetType="Button">
<Setter Property="Focusable" Value="False"/>
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
</Style>
<Style x:Key="FieldLabelStyle" TargetType="TextBlock"> <Style x:Key="FieldLabelStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource HmiMutedTextBrush}"/> <Setter Property="Foreground" Value="{StaticResource HmiMutedTextBrush}"/>
<Setter Property="FontSize" Value="13"/> <Setter Property="FontSize" Value="13"/>
@ -670,6 +675,8 @@
<Button x:Name="LoginButton" <Button x:Name="LoginButton"
Grid.Row="9" Grid.Row="9"
Content="ACCESS / LOG IN" Content="ACCESS / LOG IN"
IsDefault="True"
Focusable="True"
Style="{StaticResource CommandButtonStyle}" Style="{StaticResource CommandButtonStyle}"
Click="LogInButton_Click"/> Click="LogInButton_Click"/>
</Grid> </Grid>

15
marking_gui/StartupLoginWindow.xaml.cs

@ -46,6 +46,8 @@ namespace Housing.Login
AttachSelectionDropDowns(); AttachSelectionDropDowns();
PasswordBox.GotKeyboardFocus += (_, _) => CloseHistoryPopup(); PasswordBox.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
LoginButton.GotKeyboardFocus += (_, _) => CloseHistoryPopup(); LoginButton.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
PasswordBox.KeyDown += (s, e) => { if (e.Key == System.Windows.Input.Key.Enter) LogInButton_Click(LoginButton, e); };
OperatorTextBox.KeyDown += (s, e) => { if (e.Key == System.Windows.Input.Key.Enter) LogInButton_Click(LoginButton, e); };
MakerComboBox.Focus(); MakerComboBox.Focus();
} }
@ -65,9 +67,15 @@ namespace Housing.Login
return; return;
} }
string originalButtonText = "ACCESS / LOG IN";
if (sender is Button button) if (sender is Button button)
{ {
originalButtonText = button.Content?.ToString() ?? "ACCESS / LOG IN";
button.IsEnabled = false; button.IsEnabled = false;
button.Content = "⏳ DB 접속 확인 중...";
button.UpdateLayout();
// WPF Dispatcher 렌더링 강제 갱신으로 클릭 즉시 화면 피드백 표기
Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Render);
} }
System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait; System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
@ -109,6 +117,7 @@ namespace Housing.Login
if (sender is Button loginButton) if (sender is Button loginButton)
{ {
loginButton.IsEnabled = true; loginButton.IsEnabled = true;
loginButton.Content = originalButtonText;
} }
} }
} }
@ -135,7 +144,7 @@ namespace Housing.Login
return LoginCheckStatus.Success; return LoginCheckStatus.Success;
} }
// 3. DB 접속 및 계정 조회 (1초 하드 제한) // 3. DB 접속 및 계정 조회 (3초 하드 제한)
try try
{ {
var repository = new LoginAccountRepository( var repository = new LoginAccountRepository(
@ -143,7 +152,7 @@ namespace Housing.Login
loginSettings); loginSettings);
var dbCheckTask = repository.ExistsAsync(loginId, password); var dbCheckTask = repository.ExistsAsync(loginId, password);
var timeoutTask = Task.Delay(1000); // 1초 하드 제한 var timeoutTask = Task.Delay(3000); // 3초 하드 제한
var completedTask = await Task.WhenAny(dbCheckTask, timeoutTask); var completedTask = await Task.WhenAny(dbCheckTask, timeoutTask);
if (completedTask == dbCheckTask) if (completedTask == dbCheckTask)
@ -153,7 +162,7 @@ namespace Housing.Login
} }
else else
{ {
System.Diagnostics.Debug.WriteLine("[로그인 타임아웃] DB 응답 1초 초과 -> 타임아웃/DB연결안내 팝업 표출"); System.Diagnostics.Debug.WriteLine("[로그인 타임아웃] DB 응답 3초 초과 -> 타임아웃/DB연결안내 팝업 표출");
return LoginCheckStatus.DbTimeoutOrError; return LoginCheckStatus.DbTimeoutOrError;
} }
} }

Loading…
Cancel
Save