Browse Source

QR 스캔후 파업 자동 닫힘, 메인화면에 스캔 값 표시, PCB Barcode 표기 위치 변경 등 수정사항 반영

master
seuuung 2 days ago
parent
commit
2d8a3b0987
  1. BIN
      .vs/marking_gui.slnx/v18/.suo
  2. 34
      .vs/marking_gui.slnx/v18/DocumentLayout.backup.json
  3. 34
      .vs/marking_gui.slnx/v18/DocumentLayout.json
  4. 2
      docs/developer_manual.md
  5. 4
      marking_gui.Setup/Bundle.wxs
  6. 28
      marking_gui/App.xaml.cs
  7. 47
      marking_gui/MainWindow.xaml
  8. 477
      marking_gui/MainWindow.xaml.cs
  9. 4
      marking_gui/Manual/01_시스템_개요.md
  10. 11
      marking_gui/Manual/03_메인_화면.md
  11. 4
      marking_gui/Manual/04_통신_설정.md
  12. 18
      marking_gui/Manual/07_시험_프로세스.md
  13. 6
      marking_gui/Manual/09_오류_대응.md
  14. 229
      marking_gui/Manual/Manual_Merged.md
  15. 238
      marking_gui/Manual/Manual_for_pdf.html
  16. 2
      marking_gui/Models/Product.cs
  17. 69
      marking_gui/PortSettingsWindow.xaml
  18. 155
      marking_gui/PortSettingsWindow.xaml.cs
  19. 88
      marking_gui/QrVerificationWindow.xaml
  20. 284
      marking_gui/QrVerificationWindow.xaml.cs
  21. 163
      marking_gui/Services/BarcodeScannerSettings.cs
  22. 69
      marking_gui/Services/DatabaseService.cs
  23. 41
      marking_gui/Services/LoginServicesStub.cs
  24. 263
      marking_gui/Services/SerialBarcodeScanner.cs
  25. 19
      marking_gui/Setting.ini
  26. 45
      marking_gui/SettingsWindow.xaml
  27. 2
      marking_gui/SettingsWindow.xaml.cs
  28. 94
      marking_gui/StartupLoginWindow.xaml.cs
  29. 11
      program_flow.md

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

Binary file not shown.

34
.vs/marking_gui.slnx/v18/DocumentLayout.backup.json

@ -5,6 +5,14 @@
{ {
"AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}", "AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}" "RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
},
{
"AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\qrverificationwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\qrverificationwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
},
{
"AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\settingswindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\settingswindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
} }
], ],
"DocumentGroupContainers": [ "DocumentGroupContainers": [
@ -14,12 +22,36 @@
"DocumentGroups": [ "DocumentGroups": [
{ {
"DockedWidth": 200, "DockedWidth": 200,
"SelectedChildIndex": 1, "SelectedChildIndex": 3,
"Children": [ "Children": [
{ {
"$type": "Bookmark", "$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}" "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
}, },
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "QrVerificationWindow.xaml",
"DocumentMoniker": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeDocumentMoniker": "marking_gui\\QrVerificationWindow.xaml",
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeToolTip": "marking_gui\\QrVerificationWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:09.69Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "SettingsWindow.xaml",
"DocumentMoniker": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeDocumentMoniker": "marking_gui\\SettingsWindow.xaml",
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeToolTip": "marking_gui\\SettingsWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:06.926Z",
"EditorCaption": ""
},
{ {
"$type": "Document", "$type": "Document",
"DocumentIndex": 0, "DocumentIndex": 0,

34
.vs/marking_gui.slnx/v18/DocumentLayout.json

@ -5,6 +5,14 @@
{ {
"AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}", "AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}" "RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\mainwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
},
{
"AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\qrverificationwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\qrverificationwindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
},
{
"AbsoluteMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|c:\\users\\computer1\\desktop\\mobi\\marking_gui\\marking_gui\\settingswindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}",
"RelativeMoniker": "D:0:0:{9E96D5BE-9375-4ED3-82DE-F200F83EBC2F}|marking_gui\\marking_gui.csproj|solutionrelative:marking_gui\\settingswindow.xaml||{F11ACC28-31D1-4C80-A34B-F4E09D3D753C}"
} }
], ],
"DocumentGroupContainers": [ "DocumentGroupContainers": [
@ -14,12 +22,36 @@
"DocumentGroups": [ "DocumentGroups": [
{ {
"DockedWidth": 200, "DockedWidth": 200,
"SelectedChildIndex": 1, "SelectedChildIndex": 3,
"Children": [ "Children": [
{ {
"$type": "Bookmark", "$type": "Bookmark",
"Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}" "Name": "ST:0:0:{aa2115a1-9712-457b-9047-dbb71ca2cdd2}"
}, },
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "QrVerificationWindow.xaml",
"DocumentMoniker": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeDocumentMoniker": "marking_gui\\QrVerificationWindow.xaml",
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\QrVerificationWindow.xaml",
"RelativeToolTip": "marking_gui\\QrVerificationWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:09.69Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "SettingsWindow.xaml",
"DocumentMoniker": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeDocumentMoniker": "marking_gui\\SettingsWindow.xaml",
"ToolTip": "C:\\Users\\COMPUTER1\\Desktop\\mobi\\marking_gui\\marking_gui\\SettingsWindow.xaml",
"RelativeToolTip": "marking_gui\\SettingsWindow.xaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2026-07-29T04:12:06.926Z",
"EditorCaption": ""
},
{ {
"$type": "Document", "$type": "Document",
"DocumentIndex": 0, "DocumentIndex": 0,

2
docs/developer_manual.md

@ -56,7 +56,7 @@ LotNoTag=YYMMDD
; 제품 모델명 (Product Name) 치환 태그 ; 제품 모델명 (Product Name) 치환 태그
ProductTag=PRODUCT ProductTag=PRODUCT
; 2D QR 코드 데이터 치환 태그 ; QR 코드 데이터 치환 태그
QrCodeTag=QRCODETAG QrCodeTag=QRCODETAG
; 검사 라인 코드 치환 태그 ; 검사 라인 코드 치환 태그

4
marking_gui.Setup/Bundle.wxs

@ -1,8 +1,8 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal"> <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:bal="http://wixtoolset.org/schemas/v4/wxs/bal">
<Bundle Name="Marking System" Version="1.0.0" Manufacturer="MobiDigm" UpgradeCode="51A2E3B4-F5C6-4A7B-8C9D-0E1F2A3B4C5D" IconSourceFile="C:\marking\Marking.ico"> <Bundle Name="Marking System" Version="1.0.0" Manufacturer="MobiDigm" UpgradeCode="51A2E3B4-F5C6-4A7B-8C9D-0E1F2A3B4C5D" IconSourceFile="..\marking_gui\Marking.ico">
<BootstrapperApplication> <BootstrapperApplication>
<bal:WixStandardBootstrapperApplication LicenseUrl="https://www.mobidigm.co.kr" Theme="hyperlinkLicense" SuppressOptionsUI="yes" /> <bal:WixStandardBootstrapperApplication LicenseUrl="https://www.mobidigm.co.kr" Theme="hyperlinkLicense" SuppressOptionsUI="yes" LogoFile="..\marking_gui\Marking.ico" />
</BootstrapperApplication> </BootstrapperApplication>
<Chain> <Chain>

28
marking_gui/App.xaml.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Configuration; using System.Configuration;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
@ -13,11 +14,26 @@ namespace marking_gui
/// </summary> /// </summary>
public partial class App : Application public partial class App : Application
{ {
private static Mutex? _mutex = null;
[System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)] [System.Runtime.InteropServices.DllImport("kernel32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto, SetLastError = true)]
private static extern bool SetDllDirectory(string lpPathName); private static extern bool SetDllDirectory(string lpPathName);
protected override void OnStartup(StartupEventArgs e) protected override void OnStartup(StartupEventArgs e)
{ {
// 전역 Mutex 생성으로 프로그램 중복 실행 원천 방지
const string appName = "MarkingGui_SingleInstance_Mutex_9921";
bool createdNew;
_mutex = new Mutex(true, appName, out createdNew);
if (!createdNew)
{
MessageBox.Show("프로그램이 이미 실행 중입니다.\n\n중복 실행할 수 없습니다.", "중복 실행 방지", MessageBoxButton.OK, MessageBoxImage.Warning);
Application.Current.Shutdown();
return;
}
// 바로가기 등으로 실행 시 작업 디렉토리가 꼬이는 현상을 방지하기 위해 작업 디렉토리를 실행 파일 폴더로 강제 동기화 // 바로가기 등으로 실행 시 작업 디렉토리가 꼬이는 현상을 방지하기 위해 작업 디렉토리를 실행 파일 폴더로 강제 동기화
try try
{ {
@ -59,6 +75,18 @@ namespace marking_gui
protected override void OnExit(ExitEventArgs e) protected override void OnExit(ExitEventArgs e)
{ {
Services.LoggerService.Info("=================== [프로그램 정상 종료] ==================="); Services.LoggerService.Info("=================== [프로그램 정상 종료] ===================");
if (_mutex != null)
{
try
{
_mutex.ReleaseMutex();
_mutex.Dispose();
}
catch { }
_mutex = null;
}
base.OnExit(e); base.OnExit(e);
} }

47
marking_gui/MainWindow.xaml

@ -281,7 +281,7 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="이전 시험 단계 결과" FontSize="18" FontWeight="Bold" Foreground="#374151" Margin="0,0,0,10"/> <TextBlock Grid.Row="0" Text="이전 공정 결과" FontSize="18" FontWeight="Bold" Foreground="#374151" Margin="0,0,0,10"/>
<Grid Grid.Row="1" Margin="0,5,0,0"> <Grid Grid.Row="1" Margin="0,5,0,0">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@ -298,8 +298,14 @@
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="1. Housing" FontSize="20" Foreground="#4B5563" FontWeight="SemiBold"/> <TextBlock Text="1. Housing" FontSize="20" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock x:Name="txtPrevHousing" Grid.Column="1" Text="대기" FontSize="22" FontWeight="Bold" Foreground="#9CA3AF"/> <StackPanel Orientation="Horizontal" Margin="0,4,0,0">
<TextBlock Text="PCB Barcode: " FontSize="16" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock x:Name="txtPcbBarcode" Text="---" FontSize="16" Foreground="#1F2937" FontWeight="Bold"/>
</StackPanel>
</StackPanel>
<TextBlock x:Name="txtPrevHousing" Grid.Column="1" Text="대기" FontSize="22" FontWeight="Bold" Foreground="#9CA3AF" VerticalAlignment="Center"/>
</Grid> </Grid>
<Separator Grid.Row="1" Background="#F3F4F6"/> <Separator Grid.Row="1" Background="#F3F4F6"/>
@ -462,31 +468,38 @@
</Border> </Border>
<!-- (0,2) 2. 일련번호 정보 --> <!-- (0,2) 2. 일련번호 정보 -->
<Border Grid.Row="0" Grid.Column="2" Background="White" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="6" Padding="20,16" MinHeight="110" VerticalAlignment="Stretch"> <Border Grid.Row="0" Grid.Column="2" Background="White" BorderBrush="#E5E7EB" BorderThickness="1" CornerRadius="6" Padding="20,12" MinHeight="110" VerticalAlignment="Stretch">
<StackPanel VerticalAlignment="Center"> <StackPanel VerticalAlignment="Center">
<TextBlock Text="🏷 일련번호 정보" FontSize="18" FontWeight="Bold" Foreground="#6B7280" Margin="0,0,0,8"/> <TextBlock Text="🏷 일련번호 정보" FontSize="18" FontWeight="Bold" Foreground="#6B7280" Margin="0,0,0,6"/>
<!-- 줄바꿈하여 입력 S/N 각인 번호를 수직으로 배열하고 Viewbox로 잘림 방지 --> <!-- 줄바꿈하여 입력 S/N, Marking QR, Scan QR, 각인 번호를 수직으로 배열하고 Viewbox로 잘림 방지 -->
<StackPanel> <StackPanel>
<!-- 입력 S/N (IC) 행 --> <!-- 입력 S/N (IC) 행 -->
<Viewbox Stretch="Uniform" HorizontalAlignment="Left" MaxHeight="28" Margin="0,0,0,4"> <Viewbox Stretch="Uniform" HorizontalAlignment="Left" MaxHeight="22" Margin="0,0,0,2">
<StackPanel Orientation="Horizontal">
<TextBlock Text="IC S/N: " FontSize="18" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock x:Name="txtFinalSerial" Text="---" FontSize="18" Foreground="#1F2937" FontWeight="Bold"/>
</StackPanel>
</Viewbox>
<!-- Marking QR 행 -->
<Viewbox Stretch="Uniform" HorizontalAlignment="Left" MaxHeight="22" Margin="0,0,0,2">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="IC S/N: " FontSize="20" Foreground="#4B5563" FontWeight="SemiBold"/> <TextBlock Text="Marking QR: " FontSize="18" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock x:Name="txtFinalSerial" Text="---" FontSize="20" Foreground="#1F2937" FontWeight="Bold"/> <TextBlock x:Name="txtMarkingQr" Text="---" FontSize="18" Foreground="#1F2937" FontWeight="Bold"/>
</StackPanel> </StackPanel>
</Viewbox> </Viewbox>
<!-- PCB 바코드 행 --> <!-- Scan QR 행 -->
<Viewbox Stretch="Uniform" HorizontalAlignment="Left" MaxHeight="28" Margin="0,0,0,4"> <Viewbox Stretch="Uniform" HorizontalAlignment="Left" MaxHeight="22" Margin="0,0,0,2">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<TextBlock Text="PCB 바코드: " FontSize="20" Foreground="#4B5563" FontWeight="SemiBold"/> <TextBlock Text="Scan QR: " FontSize="18" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock x:Name="txtPcbBarcode" Text="---" FontSize="20" Foreground="#1F2937" FontWeight="Bold"/> <TextBlock x:Name="txtScanQr" Text="---" FontSize="18" Foreground="#1F2937" FontWeight="Bold"/>
</StackPanel> </StackPanel>
</Viewbox> </Viewbox>
<!-- 각인 번호 행 --> <!-- 각인 번호 행 -->
<Viewbox Stretch="Uniform" HorizontalAlignment="Left" MaxHeight="28"> <Viewbox Stretch="Uniform" HorizontalAlignment="Left" MaxHeight="22">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="각인 번호: " FontSize="20" Foreground="#4B5563" FontWeight="SemiBold" VerticalAlignment="Center"/> <TextBlock Text="각인 번호: " FontSize="18" Foreground="#4B5563" FontWeight="SemiBold" VerticalAlignment="Center"/>
<Border Background="#EFF6FF" CornerRadius="3" Padding="6,2" VerticalAlignment="Center" Margin="5,0,0,0"> <Border Background="#EFF6FF" CornerRadius="3" Padding="6,2" VerticalAlignment="Center" Margin="5,0,0,0">
<TextBlock x:Name="txtMarkedSerial" Text="---" FontSize="20" Foreground="#2563EB" FontWeight="Bold"/> <TextBlock x:Name="txtMarkedSerial" Text="---" FontSize="18" Foreground="#2563EB" FontWeight="Bold"/>
</Border> </Border>
</StackPanel> </StackPanel>
</Viewbox> </Viewbox>
@ -560,8 +573,8 @@
<Button x:Name="btnBypassStart" Content="강제 검사 (스킵)" Width="145" Height="36" <Button x:Name="btnBypassStart" Content="강제 검사 (스킵)" Width="145" Height="36"
Style="{StaticResource ModernButtonStyle}" Click="btnBypassStart_Click" Style="{StaticResource ModernButtonStyle}" Click="btnBypassStart_Click"
Margin="0,0,10,0" FontSize="13" Padding="8,0"/> Margin="0,0,10,0" FontSize="13" Padding="8,0"/>
<Button x:Name="btnTestMarking" Content="테스트 마킹" Width="120" Height="36" <Button x:Name="btnScanTest" Content="📷 스캔 테스트" Width="130" Height="36"
Style="{StaticResource ModernButtonStyle}" Click="btnTestMarking_Click" Style="{StaticResource ModernButtonStyle}" Click="btnScanTest_Click"
FontSize="13" Padding="8,0"/> FontSize="13" Padding="8,0"/>
</StackPanel> </StackPanel>

477
marking_gui/MainWindow.xaml.cs

@ -49,6 +49,7 @@ namespace marking_gui
private SerialProvider _boardSerial; private SerialProvider _boardSerial;
private Board4251Service _boardService; private Board4251Service _boardService;
private Board4251SensorService _sensorReader; private Board4251SensorService _sensorReader;
private SerialBarcodeScanner? _barcodeScanner;
public MainWindow() public MainWindow()
{ {
@ -68,6 +69,7 @@ namespace marking_gui
DisplayLoginInfo(); DisplayLoginInfo();
InitializeSensorReader(); InitializeSensorReader();
InitializeBarcodeScanner();
ApplyTestModeSettings(); ApplyTestModeSettings();
InitializeEzdFileSelection(); InitializeEzdFileSelection();
@ -91,10 +93,6 @@ namespace marking_gui
{ {
btnBypassStart.Visibility = visibility; btnBypassStart.Visibility = visibility;
} }
if (btnTestMarking != null)
{
btnTestMarking.Visibility = visibility;
}
} }
private async void InitializeMarkingDevice() private async void InitializeMarkingDevice()
@ -375,9 +373,10 @@ namespace marking_gui
if (portSettings.ShowDialog() == true) if (portSettings.ShowDialog() == true)
{ {
// 포트 설정 변경 및 저장 성공 시 센서 재연결 처리 // 포트 설정 변경 및 저장 성공 시 센서 및 바코드 스캐너 재연결 처리
InitializeSensorReader(); InitializeSensorReader();
MessageBox.Show("포트 설정이 적용되었습니다. 센서 장치를 재연결합니다.", "설정 완료", MessageBoxButton.OK, MessageBoxImage.Information); InitializeBarcodeScanner();
MessageBox.Show("포트 설정이 적용되었습니다. 센서 및 바코드 스캐너 장치를 재연결합니다.", "설정 완료", MessageBoxButton.OK, MessageBoxImage.Information);
} }
} }
@ -452,6 +451,8 @@ namespace marking_gui
} }
txtFinalSerial.Text = "---"; txtFinalSerial.Text = "---";
if (txtPcbBarcode != null) txtPcbBarcode.Text = "---"; if (txtPcbBarcode != null) txtPcbBarcode.Text = "---";
if (txtMarkingQr != null) txtMarkingQr.Text = "---";
if (txtScanQr != null) txtScanQr.Text = "---";
if (txtErrorMessage != null) if (txtErrorMessage != null)
{ {
txtErrorMessage.Text = string.Empty; txtErrorMessage.Text = string.Empty;
@ -485,10 +486,6 @@ namespace marking_gui
} }
SetStartButtonReady(); SetStartButtonReady();
if (btnTestMarking != null)
{
btnTestMarking.IsEnabled = true;
}
} }
@ -505,6 +502,28 @@ namespace marking_gui
await RunInspectionSequenceAsync(bypassPrevSteps: true); await RunInspectionSequenceAsync(bypassPrevSteps: true);
} }
// 스캔 테스트 버튼 클릭 (바코드 / QR 스캐너 연동 독립 테스트)
private void btnScanTest_Click(object sender, RoutedEventArgs e)
{
string testSampleQr = "TEST_SN_12345;DRAWING_SAMPLE;HKMC_SAMPLE;2607299999;";
var qrTestWin = new QrVerificationWindow(testSampleQr, _barcodeScanner)
{
Owner = this,
Title = "📷 바코드 / QR 스캐너 연동 테스트"
};
bool? testResult = qrTestWin.ShowDialog();
if (testResult == true)
{
MessageBox.Show("스캐너 통신 및 QR 바코드 대조 검증에 성공했습니다!\n\n스캐너 장치가 정상적으로 연동되어 있습니다.", "스캔 테스트 성공", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show("스캔 테스트가 중단되었거나 QR 대조에 실패했습니다.\n\n스캐너 포트 설정 및 연결 상태를 점검하십시오.", "스캔 테스트 종료", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private async System.Threading.Tasks.Task RunInspectionSequenceAsync(bool bypassPrevSteps) private async System.Threading.Tasks.Task RunInspectionSequenceAsync(bool bypassPrevSteps)
{ {
// 진입하자마자 즉각 버튼을 비활성화하여 중복 클릭 원천 차단 (비동기 대기 이전에 호출 필수) // 진입하자마자 즉각 버튼을 비활성화하여 중복 클릭 원천 차단 (비동기 대기 이전에 호출 필수)
@ -713,11 +732,21 @@ namespace marking_gui
if (product != null) if (product != null)
{ {
// 이전 검사 결과 바인딩 및 색상 설정 bool isPcbBarcodeValid = !string.IsNullOrWhiteSpace(product.PcbBarcode);
// 이전 검사 결과 바인딩 및 색상 설정 (PCB 바코드가 누락되면 Housing을 FAIL로 표출)
if (!isPcbBarcodeValid)
{
txtPrevHousing.Text = "FAIL";
txtPrevHousing.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
}
else
{
txtPrevHousing.Text = product.HousingResult; txtPrevHousing.Text = product.HousingResult;
txtPrevHousing.Foreground = (product.HousingResult == "PASS" || product.HousingResult == "OK") txtPrevHousing.Foreground = (product.HousingResult == "PASS" || product.HousingResult == "OK")
? new SolidColorBrush(Color.FromRgb(16, 185, 129)) ? new SolidColorBrush(Color.FromRgb(16, 185, 129))
: new SolidColorBrush(Color.FromRgb(239, 68, 68)); : new SolidColorBrush(Color.FromRgb(239, 68, 68));
}
txtPrevCal.Text = product.CalResult; txtPrevCal.Text = product.CalResult;
txtPrevCal.Foreground = (product.CalResult == "PASS" || product.CalResult == "OK") txtPrevCal.Foreground = (product.CalResult == "PASS" || product.CalResult == "OK")
@ -731,7 +760,7 @@ namespace marking_gui
if (txtPcbBarcode != null) if (txtPcbBarcode != null)
{ {
txtPcbBarcode.Text = string.IsNullOrWhiteSpace(product.PcbBarcode) ? "---" : product.PcbBarcode; txtPcbBarcode.Text = isPcbBarcodeValid ? product.PcbBarcode : "---";
} }
if (product.IsPreviousStepsPassed) if (product.IsPreviousStepsPassed)
@ -742,26 +771,62 @@ namespace marking_gui
{ {
if (bypassPrevSteps) if (bypassPrevSteps)
{ {
LoggerService.Warn(string.Format("[강제 검사 (BYPASS)] 이전 공정에 불합격이 있으나 스킵 설정이므로 진행합니다. (Housing:{0}, Cal:{1}, Eol:{2})", product.HousingResult, product.CalResult, product.EolResult)); LoggerService.Warn(string.Format("[강제 검사 (BYPASS)] 이전 공정에 불합격이 있으나 스킵 설정이므로 진행합니다. (Housing:{0}, Cal:{1}, Eol:{2}, PcbBarcodeValid:{3})", product.HousingResult, product.CalResult, product.EolResult, isPcbBarcodeValid));
shouldProceed = true; shouldProceed = true;
} }
else else
{ {
string failedStep = string.Empty; string failedStep = string.Empty;
if (product.HousingResult == "FAIL" || product.HousingResult == "NG") failedStep = "Housing 검사"; string detailErrMsg = string.Empty;
else if (product.CalResult == "FAIL" || product.CalResult == "NG") failedStep = "Calibration 검사";
else if (product.EolResult == "FAIL" || product.EolResult == "NG") failedStep = "EOL 검사";
txtSystemStatus.Text = "상태: 진행 불가 (이전 단계 불합격)"; if (!isPcbBarcodeValid)
{
failedStep = "Housing 검사 (PCB 바코드 누락)";
detailErrMsg = "하우징 공정 오류: Housing 공정의 PCB 바코드(PcbBarcode) 데이터가 누락되어 검사를 진행할 수 없습니다.";
}
else if (product.HousingResult == "FAIL" || product.HousingResult == "NG")
{
failedStep = "Housing 검사";
detailErrMsg = "이전 공정 단계(Housing 검사)가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다.";
}
else if (product.CalResult == "FAIL" || product.CalResult == "NG")
{
failedStep = "Calibration 검사";
detailErrMsg = "이전 공정 단계(Calibration 검사)가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다.";
}
else if (product.EolResult == "FAIL" || product.EolResult == "NG")
{
failedStep = "EOL 검사";
detailErrMsg = "이전 공정 단계(EOL 검사)가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다.";
}
else
{
failedStep = "선행 공정 검사";
detailErrMsg = "이전 공정 검사가 불합격 상태입니다.";
}
txtSystemStatus.Text = !isPcbBarcodeValid
? "상태: 진행 불가 (Housing PCB 바코드 누락)"
: "상태: 진행 불가 (이전 단계 불합격)";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
if (txtProgressStatus != null) if (txtProgressStatus != null)
{ {
txtProgressStatus.Text = "진행 불가 (이전 공정 불합격)"; txtProgressStatus.Text = !isPcbBarcodeValid
? "진행 불가 (Housing PCB 바코드 누락)"
: "진행 불가 (이전 공정 불합격)";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text for white card txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text for white card
} }
MessageBox.Show(string.Format("이전 공정 단계({0})가 불합격 상태이므로 최종 성능 검사를 시작할 수 없습니다.", failedStep), "검사 제한", MessageBoxButton.OK, MessageBoxImage.Error); if (txtErrorMessage != null)
{
txtErrorMessage.Text = detailErrMsg;
}
if (borderErrorBanner != null) borderErrorBanner.Visibility = Visibility.Visible;
if (borderNormalStatus != null) borderNormalStatus.Visibility = Visibility.Collapsed;
MessageBox.Show(detailErrMsg, "검사 제한 (공정 오류)", MessageBoxButton.OK, MessageBoxImage.Error);
SetStartButtonReady();
return; return;
} }
} }
@ -810,12 +875,46 @@ namespace marking_gui
txtProgressStatus.Text = "대기 상태"; txtProgressStatus.Text = "대기 상태";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(75, 85, 99)); // Gray-600 txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(75, 85, 99)); // Gray-600
} }
SetStartButtonReady();
return; return;
} }
} }
if (shouldProceed) if (shouldProceed)
{ {
// ── STEP 4. IC S/N DB 중복 여부 검사 (재작업 확인 팝업) ────────────────
bool isDuplicate = await System.Threading.Tasks.Task.Run(() => _database.IsProductExists(serial));
if (isDuplicate)
{
LoggerService.Warn($"[재작업 감지] IC S/N ({serial})가 이미 DB에 존재합니다. 작업자 확인 팝업 표시");
MessageBoxResult reworkChoice = MessageBox.Show(
string.Format("입력된 시리얼 번호({0})는 이미 검사 및 각인 이력이 존재하는 재작업 제품입니다.\n\n재작업 각인 및 검사를 계속 진행하시겠습니까?", serial),
"재작업 확인",
MessageBoxButton.YesNo,
MessageBoxImage.Warning);
if (reworkChoice != MessageBoxResult.Yes)
{
LoggerService.Info($"[재작업 취소] 작업자가 재작업 진행을 취소하였습니다. S/N: {serial}");
if (txtSystemStatus != null)
{
txtSystemStatus.Text = "상태: 검사 취소 (재작업 미진행)";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(107, 114, 128)); // Gray
}
if (txtProgressStatus != null)
{
txtProgressStatus.Text = "검사 취소 (재작업 미진행)";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(107, 114, 128));
}
SetStartButtonReady();
txtSerial.IsEnabled = true;
return;
}
}
// 4. 모두 합격(PASS) 시 자동으로 최종 성능 검사(타이머) 진행 // 4. 모두 합격(PASS) 시 자동으로 최종 성능 검사(타이머) 진행
SetStartButtonBusy(); SetStartButtonBusy();
txtSerial.IsEnabled = false; txtSerial.IsEnabled = false;
@ -1056,26 +1155,61 @@ namespace marking_gui
{ {
txtMarkedSerial.Text = markedSerial; txtMarkedSerial.Text = markedSerial;
} }
if (txtMarkingQr != null)
{
txtMarkingQr.Text = qrString;
}
bool finalMarkingPass = markingRes.Success; bool finalMarkingPass = markingRes.Success;
if (finalMarkingPass) if (finalMarkingPass)
{ {
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 성공"; if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 완료 (QR 검증 대기)";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber
LoggerService.Info($"[마킹 성공] S/N: {serial}, 각인번호: {markedSerial} -> QR 검증 팝업 오픈");
// DB 저장 전 레이저 각인 QR 코드 스캔 검증 팝업 오픈 (시리얼 스캐너 서비스 인계)
var qrVerifyWin = new QrVerificationWindow(qrString, _barcodeScanner)
{
Owner = this
};
bool? qrResult = qrVerifyWin.ShowDialog();
if (txtScanQr != null)
{
txtScanQr.Text = string.IsNullOrWhiteSpace(qrVerifyWin.ScannedQr) ? "---" : qrVerifyWin.ScannedQr;
}
if (qrResult == true)
{
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 및 QR 검증 성공";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
LoggerService.Info($"[마킹 성공] S/N: {serial}, 각인번호: {markedSerial}"); LoggerService.Info($"[QR 검증 성공] S/N: {serial}, 각인번호: {markedSerial} -> DB 저장 진행");
// QR 검증 통과 시 최종 DB 저장 및 완료 처리
SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, true, "OK", markedSerial);
} }
else else
{ {
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 실패 (" + markingRes.ErrorCode + ")"; if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 성공 / QR 검증 NG";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
LoggerService.Error($"[마킹 실패] S/N: {serial}, 각인번호: {markedSerial}, Code: {markingRes.ErrorCode}, Message: {markingRes.ErrorMessage}"); LoggerService.Warn($"[QR 검증 실패] S/N: {serial}, 각인번호: {markedSerial} -> DB NG 저장 진행");
// 마킹 실패 시에도 계측 검사는 통과했으므로 "최종 합격(마킹 실패)"으로 표출 // 마킹 성공 후 QR 검증 불일치/취소 시 NG로 DB 저장
txtFinalResult.Text = "PASS"; SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", markedSerial);
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange }
txtSystemStatus.Text = string.Format("최종 합격 (마킹 실패) [{0}]: {1}", markingRes.ErrorCode, markingRes.ErrorMessage); }
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); else
{
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 실패 (DB 미저장)";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
LoggerService.Error($"[마킹 실패] S/N: {serial}, 각인번호: {markedSerial}, Code: {markingRes.ErrorCode}, Message: {markingRes.ErrorMessage} -> DB 저장 생략");
txtFinalResult.Text = "FAIL";
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
txtSystemStatus.Text = string.Format("마킹 실패 [{0}]: {1} (DB 미저장)", markingRes.ErrorCode, markingRes.ErrorMessage);
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
if (borderVerdictCard != null) if (borderVerdictCard != null)
{ {
@ -1084,9 +1218,9 @@ namespace marking_gui
} }
MessageBox.Show( MessageBox.Show(
string.Format("레이저 각인 작업 전송 실패\n\n에러 코드: {0}\n에러 메시지: {1}\n\n하드웨어 통신선 연결, LMC 카드 전원, 혹은 설정 경로를 확인하십시오.", string.Format("레이저 각인 작업 전송 실패\n\n에러 코드: {0}\n에러 메시지: {1}\n\nDB 저장이 생략되었습니다. 하드웨어/통신 상태를 점검하십시오.",
markingRes.ErrorCode, markingRes.ErrorMessage), markingRes.ErrorCode, markingRes.ErrorMessage),
"각인 실패", MessageBoxButton.OK, MessageBoxImage.Warning); "각인 실패 (DB 미저장)", MessageBoxButton.OK, MessageBoxImage.Warning);
if (txtErrorMessage != null) if (txtErrorMessage != null)
{ {
@ -1100,10 +1234,10 @@ namespace marking_gui
{ {
borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed; borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
} }
}
// DB 저장 및 마무리 (마킹 성공 시에만 최종 OK로 저장) SetStartButtonReady();
SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, finalMarkingPass, finalMarkingPass ? "OK" : "NG", markedSerial); txtSerial.IsEnabled = true;
}
}); });
} }
catch (Exception threadEx) catch (Exception threadEx)
@ -1114,10 +1248,10 @@ namespace marking_gui
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
LoggerService.Error($"[마킹 스레드 오류] S/N: {serial}", threadEx); LoggerService.Error($"[마킹 스레드 오류] S/N: {serial}", threadEx);
txtFinalResult.Text = "PASS"; txtFinalResult.Text = "FAIL";
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
txtSystemStatus.Text = "최종 합격 (마킹 예외): " + threadEx.Message; txtSystemStatus.Text = "마킹 예외 (DB 미저장): " + threadEx.Message;
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
if (txtErrorMessage != null) if (txtErrorMessage != null)
{ {
@ -1126,8 +1260,9 @@ namespace marking_gui
if (borderErrorBanner != null) borderErrorBanner.Visibility = System.Windows.Visibility.Visible; if (borderErrorBanner != null) borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
if (borderNormalStatus != null) borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed; if (borderNormalStatus != null) borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
MessageBox.Show("마킹 비동기 스레드 실행 중 치명적인 오류가 발생했습니다:\n" + threadEx.Message + "\n" + threadEx.StackTrace, "각인 오류", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show("마킹 비동기 스레드 실행 중 치명적인 오류가 발생했습니다:\n" + threadEx.Message + "\n\nDB 저장이 생략되었습니다.", "각인 오류", MessageBoxButton.OK, MessageBoxImage.Error);
SaveAndFinalize(serial, finalVoltage, finalCurrent, finalResistance, false, "NG", "ERR_SERIAL"); SetStartButtonReady();
txtSerial.IsEnabled = true;
}); });
} }
}); });
@ -1246,7 +1381,7 @@ namespace marking_gui
} }
else else
{ {
txtProgressStatus.Text = "최종 불합격 (DB 저장 성공)"; txtProgressStatus.Text = (result == "NG") ? "각인 QR 검증 실패 (DB NG 저장 완료)" : "최종 불합격 (DB 저장 성공)";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red text
} }
} }
@ -1636,266 +1771,20 @@ namespace marking_gui
LoggerService.Info($"EZD 도안 파일 선택 완료: {fileName} (절대경로: {fullPath})"); LoggerService.Info($"EZD 도안 파일 선택 완료: {fileName} (절대경로: {fullPath})");
} }
// 테스트 마킹 버튼 클릭 (나머지 다 통과 가정한 레이저 각인 테스트) private void InitializeBarcodeScanner()
private async void btnTestMarking_Click(object sender, RoutedEventArgs e)
{ {
try try
{ {
string serial = txtSerial.Text.Trim(); string iniPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
var settings = BarcodeScannerSettings.Load(iniPath);
// S/N 가 비어있는 경우, 테스트 마킹용 임시 S/N 발급 _barcodeScanner?.Stop();
if (string.IsNullOrEmpty(serial)) _barcodeScanner = new SerialBarcodeScanner(settings);
{ _barcodeScanner.Start();
serial = "T" + DateTime.Now.ToString("HHmmss"); LoggerService.Info($"[바코드 스캐너] 서비스 오픈 완료 (Enabled: {settings.Enabled}, Port: {settings.PortName}, Baud: {settings.BaudRate})");
txtSerial.Text = serial;
LoggerService.Info($"[테스트 각인] S/N가 비어있어 가상 S/N 발급: {serial}");
}
LoggerService.Info($"----------------- [테스트 각인 진행 시작] S/N: {serial} -----------------");
// 새로운 검사 진행을 위해 UI 상태를 초기화
ResetUI();
// UI 상태 복원 및 진행 개시 알림
txtSerial.Text = serial;
txtFinalSerial.Text = serial;
txtSystemStatus.Text = "상태: 테스트 마킹 시작 (이전 단계 패스 가정)...";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
txtFinalResult.Text = "테스트 중";
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue text
if (borderVerdictCard != null)
{
borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(239, 246, 255)); // Light Blue background
borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue border
borderVerdictCard.BorderThickness = new Thickness(2);
}
if (badgeFinalResult != null)
{
badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white
badgeFinalResult.BorderBrush = new SolidColorBrush(Color.FromRgb(229, 231, 235)); // Gray
}
borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
if (txtProgressStatus != null)
{
txtProgressStatus.Text = "[테스트] 모의 계측 진행 중...";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue text
}
SetStartButtonBusy();
btnTestMarking.IsEnabled = false;
txtSerial.IsEnabled = false;
// 이전 단계 결과를 강제로 다 OK로 설정
txtPrevHousing.Text = "OK";
txtPrevHousing.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
txtPrevCal.Text = "OK";
txtPrevCal.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
txtPrevEol.Text = "OK";
txtPrevEol.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
// 시작 시간 기록 및 UI 출력
_testStartTime = DateTime.Now;
txtTestStartTime.Text = "시작 시간: " + _testStartTime.ToString("yyyy-MM-dd HH:mm:ss");
txtTestEndTime.Text = "종료 시간: --:--:--";
// 측정값을 정상 범위의 중간값으로 가정하여 설정 (전압 5.0V, 전류 9.0A, 저항 0.6Ω)
double mockVoltage = (_limits.MinVoltage + _limits.MaxVoltage) / 2.0;
double mockCurrent = (_limits.MinCurrent + _limits.MaxCurrent) / 2.0;
double mockResistance = (_limits.MinResistance + _limits.MaxResistance) / 2.0;
txtVoltage.Text = mockVoltage.ToString("F2");
txtCurrent.Text = mockCurrent.ToString("F2");
txtResistance.Text = mockResistance.ToString("00.000");
txtVoltage.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
txtCurrent.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
txtResistance.Foreground = new SolidColorBrush(Color.FromRgb(31, 41, 55));
if (borderVoltage != null)
{
borderVoltage.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E"));
borderVoltage.BorderThickness = new Thickness(3);
}
if (borderCurrent != null)
{
borderCurrent.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E"));
borderCurrent.BorderThickness = new Thickness(3);
}
if (borderResistance != null)
{
borderResistance.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#22C55E"));
borderResistance.BorderThickness = new Thickness(3);
}
// 최종 판정 OK 설정
txtSystemStatus.Text = "상태: 검사 완료 (합격 가정)";
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129));
txtFinalResult.Text = "OK";
txtFinalResult.Foreground = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green text for white card
if (borderVerdictCard != null)
{
borderVerdictCard.Background = new SolidColorBrush(Color.FromRgb(240, 253, 250)); // Light Green background
borderVerdictCard.BorderBrush = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green border
borderVerdictCard.BorderThickness = new Thickness(3);
}
if (badgeFinalResult != null)
{
badgeFinalResult.Background = System.Windows.Media.Brushes.White; // Keep Progress Status banner white
}
borderFinalResult.Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(243, 244, 246)); // Default Light Gray container
// 마킹 상태 설정
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 중...";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange
if (txtProgressStatus != null)
{
txtProgressStatus.Text = "[테스트] 레이저 각인 진행 중...";
txtProgressStatus.Foreground = new SolidColorBrush(Color.FromRgb(245, 158, 11)); // Amber/Orange text
}
// 비동기 마킹 동작 수행
await System.Threading.Tasks.Task.Run(async () =>
{
try
{
// 1. 테스트용 각인 일련번호 생성 (yyMMdd + 4자리 시퀀스)
string currentLot = string.IsNullOrEmpty(_loginInfo.LotNo) || _loginInfo.LotNo == "---" ? "TESTLOT" : (_loginInfo.LotNo.Trim().Length < 2 ? _loginInfo.LotNo.Trim().PadLeft(2, '0') : _loginInfo.LotNo.Trim());
int seq;
try
{
seq = await GetNextMarkingSequenceAsync(currentLot);
}
catch (Exception seqEx)
{
LoggerService.Error("[테스트 순번 조회 실패] DB 순번 조회 실패", seqEx);
Dispatcher.Invoke(() =>
{
string errMsg = $"각인 순번 DB 조회 실패: {seqEx.Message}";
if (txtSystemStatus != null)
{
txtSystemStatus.Text = errMsg;
txtSystemStatus.Foreground = new SolidColorBrush(Color.FromRgb(239, 68, 68));
}
if (txtErrorMessage != null) txtErrorMessage.Text = errMsg;
if (borderErrorBanner != null) borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
if (borderNormalStatus != null) borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
MessageBox.Show($"각인 순번을 DB에서 가져오지 못했습니다.\n\n{seqEx.Message}\n\nDB 연결 상태를 확인하십시오.", "순번 조회 실패", MessageBoxButton.OK, MessageBoxImage.Error);
});
return;
}
string yymmdd = DateTime.Now.ToString("yyMMdd");
string markedSerial = string.Format("{0}{1}{2:D4}", yymmdd, currentLot, seq);
string testSerial = string.IsNullOrEmpty(serial) ? "TEST_IC_SN" : serial;
string drawingNo = _markingConfig.DrawingNo;
string hkmcNo = _markingConfig.HkmcNo;
string qrString = string.Format("{0};{1};{2};{3};", testSerial, drawingNo, hkmcNo, markedSerial);
// 마킹 스캔 요청 데이터 빌드
var req = new MarkingRequest
{
Serial = markedSerial,
IcSerial = testSerial,
LotNo = currentLot,
ProductNo = string.IsNullOrEmpty(_loginInfo.Model) || _loginInfo.Model == "---" ? "AIC" : _loginInfo.Model,
DrawingNo = drawingNo,
HkmcNo = hkmcNo,
EzdFileName = _currentEzdFile,
LineCode = string.IsNullOrEmpty(_loginInfo.LineNo) || _loginInfo.LineNo == "---" ? "L1" : _loginInfo.LineNo,
ShiftCode = "A", // 기본 1조
QrString = qrString
};
MarkingResult markingRes;
if (_database.OfflineMode)
{
// 오프라인 모드 시뮬레이션
await System.Threading.Tasks.Task.Delay(500);
markingRes = new MarkingResult
{
Success = true,
ErrorCode = 0,
ErrorMessage = "오프라인 모드 각인 완료 모사",
MarkedAt = DateTime.Now
};
}
else
{
markingRes = _markingService.RequestMarking(req);
}
// UI 스레드로 컴백
Dispatcher.Invoke(() =>
{
if (txtMarkedSerial != null)
{
txtMarkedSerial.Text = markedSerial;
}
if (markingRes.Success)
{
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 성공";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(16, 185, 129)); // Green
LoggerService.Info($"[테스트 각인 성공] S/N: {serial}, 각인번호: {markedSerial}");
}
else
{
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 실패 (" + markingRes.ErrorCode + ")";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
LoggerService.Warn($"[테스트 각인 실패] S/N: {serial}, 각인번호: {markedSerial}, Code: {markingRes.ErrorCode}, Message: {markingRes.ErrorMessage}");
MessageBox.Show(
string.Format("테스트 레이저 각인 작업 전송 실패\n\n에러 코드: {0}\n에러 메시지: {1}\n\n장치 초기화 및 설정 상태를 점검하십시오.",
markingRes.ErrorCode, markingRes.ErrorMessage),
"테스트 각인 실패", MessageBoxButton.OK, MessageBoxImage.Warning);
if (txtErrorMessage != null)
{
txtErrorMessage.Text = string.Format("테스트 각인 에러: {0} ({1})", markingRes.ErrorMessage, markingRes.ErrorCode);
}
if (borderErrorBanner != null)
{
borderErrorBanner.Visibility = System.Windows.Visibility.Visible;
}
if (borderNormalStatus != null)
{
borderNormalStatus.Visibility = System.Windows.Visibility.Collapsed;
}
}
// DB 저장 및 마무리 (마킹 성공 시 최종 OK로 저장)
SaveAndFinalize(serial, mockVoltage, mockCurrent, mockResistance, true, markingRes.Success ? "OK" : "NG", markedSerial);
// 테스트 마킹 버튼 활성화 복구
btnTestMarking.IsEnabled = true;
});
}
catch (Exception threadEx)
{
Dispatcher.Invoke(() =>
{
if (txtMarkingStatus != null) txtMarkingStatus.Text = "마킹 에러";
if (borderMarkingStatus != null) borderMarkingStatus.Background = new SolidColorBrush(Color.FromRgb(239, 68, 68));
LoggerService.Error($"[테스트 각인 비동기 스레드 에러] S/N: {serial}", threadEx);
MessageBox.Show("각인 하드웨어 제어 중 치명적인 예외가 발생했습니다:\n" + threadEx.Message + "\n" + threadEx.StackTrace, "마킹 엔진 오류", MessageBoxButton.OK, MessageBoxImage.Error);
btnTestMarking.IsEnabled = true;
SetStartButtonReady();
txtSerial.IsEnabled = true;
});
}
});
} }
catch (Exception ex) catch (Exception ex)
{ {
MessageBox.Show("테스트 마킹 프로세스 진행 중 예외 발생:\n" + ex.Message + "\n" + ex.StackTrace, "오류", MessageBoxButton.OK, MessageBoxImage.Error); LoggerService.Error("[바코드 스캐너] 서비스 초기화 중 예외", ex);
btnTestMarking.IsEnabled = true;
SetStartButtonReady();
txtSerial.IsEnabled = true;
} }
} }
private async Task<int> GetNextMarkingSequenceAsync(string lotNo) private async Task<int> GetNextMarkingSequenceAsync(string lotNo)

4
marking_gui/Manual/01_시스템_개요.md

@ -19,7 +19,7 @@
| **자동 계측 시퀀스** | 전원 공급 장치와 전압/저항 측정 장비들과 연동하여 제품에 테스트 전원을 투입하고 전압, 저항, 전류 값을 차례대로 계측합니다. | | **자동 계측 시퀀스** | 전원 공급 장치와 전압/저항 측정 장비들과 연동하여 제품에 테스트 전원을 투입하고 전압, 저항, 전류 값을 차례대로 계측합니다. |
| **실제 저항값 측정** | 전압/저항 DMM 측정기를 통해 센서의 실제 저항값(kΩ)을 측정합니다. | | **실제 저항값 측정** | 전압/저항 DMM 측정기를 통해 센서의 실제 저항값(kΩ)을 측정합니다. |
| **전류 측정** | 전원 공급 장치(Power Supply)의 출력 채널 1을 통해 제품의 소모 전류(mA)를 계측합니다. | | **전류 측정** | 전원 공급 장치(Power Supply)의 출력 채널 1을 통해 제품의 소모 전류(mA)를 계측합니다. |
| **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 2D QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. | | **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. |
| **결과 기록** | 계측 판정 규격(전압, 전류, 저항)과 마킹 성공 여부를 최종 판정하여 생산 데이터베이스에 저장합니다. | | **결과 기록** | 계측 판정 규격(전압, 전류, 저항)과 마킹 성공 여부를 최종 판정하여 생산 데이터베이스에 저장합니다. |
--- ---
@ -44,7 +44,7 @@
| **센서 판독 보드 (4251 보드)** | 제품 내부 칩의 일련번호(IC_SN)를 인식합니다. | RS-232 시리얼 통신 | | **센서 판독 보드 (4251 보드)** | 제품 내부 칩의 일련번호(IC_SN)를 인식합니다. | RS-232 시리얼 통신 |
| **전압 및 저항 측정 장비 (DMM)** | 제품의 전압(V) 및 전기적 저항(kΩ)을 측정합니다. | TCP/IP 네트워크 통신 | | **전압 및 저항 측정 장비 (DMM)** | 제품의 전압(V) 및 전기적 저항(kΩ)을 측정합니다. | TCP/IP 네트워크 통신 |
| **전류 측정 및 전원 공급 장비 (PSU)** | 제품 구동을 위한 전원 공급 및 소모 전류(mA)를 측정합니다. | TCP/IP 네트워크 통신 | | **전류 측정 및 전원 공급 장비 (PSU)** | 제품 구동을 위한 전원 공급 및 소모 전류(mA)를 측정합니다. | TCP/IP 네트워크 통신 |
| **레이저 마킹기** | 2D QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 | | **레이저 마킹기** | QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 |
## 1.4 설치 경로(`C:\marking\`) 구조 및 생성 파일 역할 ## 1.4 설치 경로(`C:\marking\`) 구조 및 생성 파일 역할

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

@ -37,7 +37,8 @@
![[Pasted image 20260728100343.png]] ![[Pasted image 20260728100343.png]]
### 3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단) ### 3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단)
생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다. 생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다.
* **1. Housing**: 하우징 조립 공정 검사 판정 상태 (`PASS` / `FAIL` / `대기`) * **1. Housing**: 하우징 조립 공정 검사 판정 상태 (`PASS` / `FAIL` / `대기`) 및 서브 텍스트로 **`PCB Barcode: [값]`**을 함께 표시합니다.
* *주의: Housing 검사가 PASS이더라도 PCB 바코드 값을 읽어오지 못하거나 누락된 경우 Housing 항목은 **FAIL**로 처리됩니다.*
* **2. Calibration**: 센서 압력 보정 공정 판정 상태 (`PASS` / `FAIL` / `대기`) * **2. Calibration**: 센서 압력 보정 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
* **3. EOL**: EOL 1차 성능 평가 공정 판정 상태 (`PASS` / `FAIL` / `대기`) * **3. EOL**: EOL 1차 성능 평가 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
* *주의: 이전 공정 이력 중 하나라도 `FAIL` 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)* * *주의: 이전 공정 이력 중 하나라도 `FAIL` 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)*
@ -51,12 +52,12 @@
![[Pasted image 20260728100404.png]] ![[Pasted image 20260728100404.png]]
### 3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단) ### 3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)
진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다. 진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다.
* **💬 진행 상황 뱃지**: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 설비 시퀀스 동작을 실시간 텍스트로 표출합니다. * **💬 진행 상황 뱃지**: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중...", "각인 QR 검증 실패 (DB NG 저장 완료)" 등 설비 시퀀스 및 검증 동작을 실시간 텍스트로 표출합니다.
* **세부 정보 그리드**: * **세부 정보 그리드**:
- **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출 - **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- **일련번호 정보**: IC S/N, PCB 바코드, 실제 각인 완료 번호 표출 - **일련번호 정보**: `IC S/N`, **`Marking QR :`** (마킹 생성 QR 값), **`Scan QR :`** (스캐너 실측 QR 값), `각인 번호` 4개 항목을 수직 배열로 표출
- **최종 판정결과**: 최종 **PASS** (초록색) / **FAIL** (빨간색) 대형 배지 표출 - **최종 판정결과**: 최종 **PASS/OK** (초록색) / **FAIL/NG** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **NG**로 자동 반영됨)
- **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러 메시지 배너 표출 - **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
![[Pasted image 20260728100413.png]] ![[Pasted image 20260728100413.png]]
--- ---

4
marking_gui/Manual/04_통신_설정.md

@ -23,6 +23,10 @@
- 드롭다운 메뉴를 통해 **`1`** 또는 **`2`** 채널을 선택할 수 있습니다. - 드롭다운 메뉴를 통해 **`1`** 또는 **`2`** 채널을 선택할 수 있습니다.
- 선택된 채널 및 포트 정보는 `Sensor.ini` 파일에 저장됩니다. - 선택된 채널 및 포트 정보는 `Sensor.ini` 파일에 저장됩니다.
4. **바코드 / QR 스캐너 포트 (Scanner Port)**:
- 각인 후 QR 검증 시 사용되는 바코드/QR 스캐너의 연결 포트를 지정합니다. (`Auto` 또는 특정 `COM` 포트 선택)
- `Auto` 선택 시 입력 수신 단자를 자동으로 자동 인식합니다.
--- ---
## 4.2 설정 저장 ## 4.2 설정 저장

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

@ -67,7 +67,21 @@ graph TD
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다. * 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (PASS)**: 2. **모든 항목이 합격인 경우 (PASS)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다. * 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
* 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다. * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
* 마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다. * 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
---
## 7.4 레이저 각인 QR 검증 팝업 및 결과 연동
레이저 마킹이 완료되면 검증을 위해 **QR 검증 팝업 창**이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
1. **QR 스캔 검증 성공 (즉시 자동 닫힘)**
- 스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 **즉시 자동으로 닫히며** DB에 `OK` 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
2. **QR 스캔 검증 실패 (NG 판정 및 설명 표출)**
- 스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
- 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 즉시 전환됩니다.
- 공정 진행 상태 및 시스템 상태 바에 `"마킹 완료 / 각인 QR 검증 NG"`, `"상태: 검사 불합격 (각인 QR 검증 실패)"`, `"각인 QR 검증 실패 (DB NG 저장 완료)"` 등 구체적인 원인 설명이 표시되며, DB에 최종 `NG` 결과로 기록됩니다.

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

@ -42,6 +42,12 @@
- **순번 범위 초과**: 현재 로트 번호(`Lot No`)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다. - **순번 범위 초과**: 현재 로트 번호(`Lot No`)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다.
- **해결 방법**: 새로운 생산 조 또는 로트 번호(`Lot No`)를 수정하여 **프로그램에 재로그인**하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다. - **해결 방법**: 새로운 생산 조 또는 로트 번호(`Lot No`)를 수정하여 **프로그램에 재로그인**하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다.
### 9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오."
* **현상**: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.
* **원인 및 해결**:
1. **네트워크/DB 서버 점검**: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. `Database.ini` 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
2. **계정 정보 확인**: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 `LoginOptions.json` 계정 목록에 올바르게 등록되어 있는지 재확인하십시오.
--- ---
## 9.2 프로그램 정상 종료 및 전원 자동 차단 규칙 ## 9.2 프로그램 정상 종료 및 전원 자동 차단 규칙

229
marking_gui/Manual/Manual_Merged.md

@ -286,7 +286,7 @@ a:hover {
.toc-list { .toc-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 16px;
} }
.toc-item { .toc-item {
@ -294,24 +294,6 @@ a:hover {
align-items: baseline; align-items: baseline;
font-size: 11pt; font-size: 11pt;
line-height: 1.5; line-height: 1.5;
margin-top: 6px;
}
.toc-sub-item {
display: flex;
align-items: baseline;
font-size: 9.5pt;
line-height: 1.4;
padding-left: 18px;
}
.toc-sub2-item {
display: flex;
align-items: baseline;
font-size: 9pt;
line-height: 1.3;
padding-left: 36px;
color: #6b7280;
} }
.toc-title { .toc-title {
@ -321,29 +303,11 @@ a:hover {
padding-right: 6px; padding-right: 6px;
} }
.toc-sub-item .toc-title {
font-weight: 500;
color: #374151;
}
.toc-sub2-item .toc-title {
font-weight: 400;
color: #4b5563;
}
.toc-title a { .toc-title a {
color: #1f2937 !important; color: #1f2937 !important;
text-decoration: none !important; text-decoration: none !important;
} }
.toc-sub-item .toc-title a {
color: #374151 !important;
}
.toc-sub2-item .toc-title a {
color: #4b5563 !important;
}
.toc-title a:hover { .toc-title a:hover {
color: #007acc !important; color: #007acc !important;
} }
@ -356,14 +320,6 @@ a:hover {
top: -4px; top: -4px;
} }
.toc-sub-item .toc-leader {
border-bottom: 1px dotted #cbd5e1;
}
.toc-sub2-item .toc-leader {
border-bottom: 1px dotted #e2e8f0;
}
.toc-page { .toc-page {
font-weight: 700; font-weight: 700;
color: #374151; color: #374151;
@ -371,16 +327,6 @@ a:hover {
white-space: nowrap; white-space: nowrap;
} }
.toc-sub-item .toc-page {
font-weight: 600;
color: #4b5563;
}
.toc-sub2-item .toc-page {
font-weight: 400;
color: #6b7280;
}
</style> </style>
@ -398,176 +344,46 @@ a:hover {
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">3</span> <span class="toc-page">3</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#11-시스템-개요">1.1 시스템 개요</a></span>
<span class="toc-leader"></span>
<span class="toc-page">3</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#12-소프트웨어-요구사항">1.2 소프트웨어 요구사항</a></span>
<span class="toc-leader"></span>
<span class="toc-page">3</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#13-하드웨어-요구사항">1.3 하드웨어 요구사항</a></span>
<span class="toc-leader"></span>
<span class="toc-page">4</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#14-설치-경로-및-파일-배치-구조">1.4 설치 경로 및 파일 배치 구조</a></span>
<span class="toc-leader"></span>
<span class="toc-page">4</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#2-초기-설정-가이드">2. 초기 설정 가이드</a></span> <span class="toc-title"><a href="#2-초기-설정-가이드">2. 초기 설정 가이드</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">5</span> <span class="toc-page">5</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#21-프로그램-설정-구성-개요">2.1 프로그램 설정 구성 개요</a></span>
<span class="toc-leader"></span>
<span class="toc-page">5</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#22-메인-설정-settingini-세부-규격">2.2 메인 설정 (Setting.ini) 세부 규격</a></span>
<span class="toc-leader"></span>
<span class="toc-page">6</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#23-데이터베이스-설정-databaseini">2.3 데이터베이스 설정 (Database.ini)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">7</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#24-센서-통신-설정-sensorini">2.4 센서 통신 설정 (Sensor.ini)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">7</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#3-메인-화면-home-view">3. 메인 화면 (Home View)</a></span> <span class="toc-title"><a href="#3-메인-화면-home-view">3. 메인 화면 (Home View)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">8</span> <span class="toc-page">8</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="03_메인_화면.md#31-메인-화면-레이아웃-구성">3.1 메인 화면 레이아웃 구성</a></span>
<span class="toc-leader"></span>
<span class="toc-page">8</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="03_메인_화면.md#32-상태-및-제어-카드-영역">3.2 상태 및 제어 카드 영역</a></span>
<span class="toc-leader"></span>
<span class="toc-page">8</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="03_메인_화면.md#33-하단-제어-영역">3.3 하단 제어 영역 (Control Box & Footer)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">10</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#4-통신-설정-comm-settings">4. 통신 설정 (Comm. Settings)</a></span> <span class="toc-title"><a href="#4-통신-설정-comm-settings">4. 통신 설정 (Comm. Settings)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">11</span> <span class="toc-page">11</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="04_통신_설정.md#41-포트-설정-화면-사용법">4.1 포트 설정 화면 사용법</a></span>
<span class="toc-leader"></span>
<span class="toc-page">11</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="04_통신_설정.md#42-설정-저장">4.2 설정 저장</a></span>
<span class="toc-leader"></span>
<span class="toc-page">11</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#5-시험-설정-parameters">5. 시험 설정 (Parameters)</a></span> <span class="toc-title"><a href="#5-시험-설정-parameters">5. 시험 설정 (Parameters)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">12</span> <span class="toc-page">12</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="05_시험_설정.md#51-판정-기준-설정">5.1 판정 기준 설정</a></span>
<span class="toc-leader"></span>
<span class="toc-page">12</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="05_시험_설정.md#52-레이저-각인-도면-지정">5.2 레이저 각인 도면 지정</a></span>
<span class="toc-leader"></span>
<span class="toc-page">13</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="05_시험_설정.md#53-설정-저장-및-적용">5.3 설정 저장 및 적용</a></span>
<span class="toc-leader"></span>
<span class="toc-page">13</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#6-측정-데이터-조회-및-데이터베이스-연동">6. 측정 데이터 조회 (Data View)</a></span> <span class="toc-title"><a href="#6-측정-데이터-조회-및-데이터베이스-연동">6. 측정 데이터 조회 (Data View)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">14</span> <span class="toc-page">14</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="06_데이터_조회.md#61-선행-공정-결과-조회">6.1 선행 공정 결과 조회 (이력 확인)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">14</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="06_데이터_조회.md#62-검사-결과-자동-적재-및-최종-판정-기록">6.2 검사 결과 자동 적재 및 최종 판정 기록</a></span>
<span class="toc-leader"></span>
<span class="toc-page">15</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="06_데이터_조회.md#63-오프라인-모드-운용">6.3 오프라인 모드 운용</a></span>
<span class="toc-leader"></span>
<span class="toc-page">15</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#7-자동-시험-프로세스-및-14단계-시퀀스">7. 자동 시험 프로세스</a></span> <span class="toc-title"><a href="#7-자동-시험-프로세스-및-14단계-시퀀스">7. 자동 시험 프로세스</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">16</span> <span class="toc-page">16</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="07_시험_프로세스.md#71-자동-시험-및-마킹-프로세스-개요">7.1 자동 시험 및 마킹 프로세스 개요</a></span>
<span class="toc-leader"></span>
<span class="toc-page">16</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="07_시험_프로세스.md#72-상세-15단계-자동-제어-시퀀스">7.2 상세 15단계 자동 제어 시퀀스</a></span>
<span class="toc-leader"></span>
<span class="toc-page">16</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="07_시험_프로세스.md#73-최종-판정-및-마킹-실행-조건">7.3 최종 판정 및 마킹 실행 조건</a></span>
<span class="toc-leader"></span>
<span class="toc-page">17</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#8-로그-및-데이터-관리">8. 로그 및 데이터 관리</a></span> <span class="toc-title"><a href="#8-로그-및-데이터-관리">8. 로그 및 데이터 관리</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">18</span> <span class="toc-page">18</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="08_로그_관리.md#81-시스템-로그-수집-및-관리">8.1 시스템 로그 수집 및 관리</a></span>
<span class="toc-leader"></span>
<span class="toc-page">18</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="08_로그_관리.md#82-장비-교체-및-프로그램-백업-방법">8.2 장비 교체 및 프로그램 백업 방법</a></span>
<span class="toc-leader"></span>
<span class="toc-page">18</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#9-오류-대응-및-프로그램-종료">9. 오류 대응 및 프로그램 종료</a></span> <span class="toc-title"><a href="#9-오류-대응-및-프로그램-종료">9. 오류 대응 및 프로그램 종료</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">19</span> <span class="toc-page">19</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="09_오류_대응.md#91-현장-장애-유형별-점검-및-예외-조치-요령">9.1 현장 장애 유형별 점검 및 예외 조치 요령</a></span>
<span class="toc-leader"></span>
<span class="toc-page">19</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="09_오류_대응.md#92-프로그램-정상-종료-및-전원-자동-차단-규칙">9.2 프로그램 정상 종료 및 전원 자동 차단 규칙</a></span>
<span class="toc-leader"></span>
<span class="toc-page">20</span>
</div>
</div> </div>
</div> </div>
@ -613,7 +429,7 @@ a:hover {
| **자동 계측 시퀀스** | 전원 공급 장치와 전압/저항 측정 장비들과 연동하여 제품에 테스트 전원을 투입하고 전압, 저항, 전류 값을 차례대로 계측합니다. | | **자동 계측 시퀀스** | 전원 공급 장치와 전압/저항 측정 장비들과 연동하여 제품에 테스트 전원을 투입하고 전압, 저항, 전류 값을 차례대로 계측합니다. |
| **실제 저항값 측정** | 전압/저항 DMM 측정기를 통해 센서의 실제 저항값(kΩ)을 측정합니다. | | **실제 저항값 측정** | 전압/저항 DMM 측정기를 통해 센서의 실제 저항값(kΩ)을 측정합니다. |
| **전류 측정** | 전원 공급 장치(Power Supply)의 출력 채널 1을 통해 제품의 소모 전류(mA)를 계측합니다. | | **전류 측정** | 전원 공급 장치(Power Supply)의 출력 채널 1을 통해 제품의 소모 전류(mA)를 계측합니다. |
| **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 2D QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. | | **레이저 각인 연동** | 성능 검사가 모두 통과된 합격품에 대해 제품 표면에 QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다. |
| **결과 기록** | 계측 판정 규격(전압, 전류, 저항)과 마킹 성공 여부를 최종 판정하여 생산 데이터베이스에 저장합니다. | | **결과 기록** | 계측 판정 규격(전압, 전류, 저항)과 마킹 성공 여부를 최종 판정하여 생산 데이터베이스에 저장합니다. |
--- ---
@ -638,7 +454,7 @@ a:hover {
| **센서 판독 보드 (4251 보드)** | 제품 내부 칩의 일련번호(IC_SN)를 인식합니다. | RS-232 시리얼 통신 | | **센서 판독 보드 (4251 보드)** | 제품 내부 칩의 일련번호(IC_SN)를 인식합니다. | RS-232 시리얼 통신 |
| **전압 및 저항 측정 장비 (DMM)** | 제품의 전압(V) 및 전기적 저항(kΩ)을 측정합니다. | TCP/IP 네트워크 통신 | | **전압 및 저항 측정 장비 (DMM)** | 제품의 전압(V) 및 전기적 저항(kΩ)을 측정합니다. | TCP/IP 네트워크 통신 |
| **전류 측정 및 전원 공급 장비 (PSU)** | 제품 구동을 위한 전원 공급 및 소모 전류(mA)를 측정합니다. | TCP/IP 네트워크 통신 | | **전류 측정 및 전원 공급 장비 (PSU)** | 제품 구동을 위한 전원 공급 및 소모 전류(mA)를 측정합니다. | TCP/IP 네트워크 통신 |
| **레이저 마킹기** | 2D QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 | | **레이저 마킹기** | QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다. | 마킹 제어 보드 연동 |
## 1.4 설치 경로(`C:\marking\`) 구조 및 생성 파일 역할 ## 1.4 설치 경로(`C:\marking\`) 구조 및 생성 파일 역할
@ -823,7 +639,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<img src="./Pasted image 20260728100343.png" alt="Pasted image 20260728100343.png" class="img-screenshot" /> <img src="./Pasted image 20260728100343.png" alt="Pasted image 20260728100343.png" class="img-screenshot" />
### 3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단) ### 3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단)
생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다. 생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다.
* **1. Housing**: 하우징 조립 공정 검사 판정 상태 (`PASS` / `FAIL` / `대기`) * **1. Housing**: 하우징 조립 공정 검사 판정 상태 (`PASS` / `FAIL` / `대기`) 및 서브 텍스트로 **`PCB Barcode: [값]`**을 함께 표시합니다.
* *주의: Housing 검사가 PASS이더라도 PCB 바코드 값을 읽어오지 못하거나 누락된 경우 Housing 항목은 **FAIL**로 처리됩니다.*
* **2. Calibration**: 센서 압력 보정 공정 판정 상태 (`PASS` / `FAIL` / `대기`) * **2. Calibration**: 센서 압력 보정 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
* **3. EOL**: EOL 1차 성능 평가 공정 판정 상태 (`PASS` / `FAIL` / `대기`) * **3. EOL**: EOL 1차 성능 평가 공정 판정 상태 (`PASS` / `FAIL` / `대기`)
* *주의: 이전 공정 이력 중 하나라도 `FAIL` 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)* * *주의: 이전 공정 이력 중 하나라도 `FAIL` 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)*
@ -837,12 +654,12 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<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] 최종 판정 및 종합 상태 영역 (우측 하단)
진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다. 진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다.
* **💬 진행 상황 뱃지**: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 설비 시퀀스 동작을 실시간 텍스트로 표출합니다. * **💬 진행 상황 뱃지**: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중...", "각인 QR 검증 실패 (DB NG 저장 완료)" 등 설비 시퀀스 및 검증 동작을 실시간 텍스트로 표출합니다.
* **세부 정보 그리드**: * **세부 정보 그리드**:
- **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출 - **검사 시간 정보**: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- **일련번호 정보**: IC S/N, PCB 바코드, 실제 각인 완료 번호 표출 - **일련번호 정보**: `IC S/N`, **`Marking QR :`** (마킹 생성 QR 값), **`Scan QR :`** (스캐너 실측 QR 값), `각인 번호` 4개 항목을 수직 배열로 표출
- **최종 판정결과**: 최종 **PASS** (초록색) / **FAIL** (빨간색) 대형 배지 표출 - **최종 판정결과**: 최종 **PASS/OK** (초록색) / **FAIL/NG** (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 **NG**로 자동 반영됨)
- **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러 메시지 배너 표출 - **시스템 상태**: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
<img src="./Pasted image 20260728100413.png" alt="Pasted image 20260728100413.png" class="img-screenshot" /> <img src="./Pasted image 20260728100413.png" alt="Pasted image 20260728100413.png" class="img-screenshot" />
--- ---
@ -886,6 +703,10 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
- 드롭다운 메뉴를 통해 **`1`** 또는 **`2`** 채널을 선택할 수 있습니다. - 드롭다운 메뉴를 통해 **`1`** 또는 **`2`** 채널을 선택할 수 있습니다.
- 선택된 채널 및 포트 정보는 `Sensor.ini` 파일에 저장됩니다. - 선택된 채널 및 포트 정보는 `Sensor.ini` 파일에 저장됩니다.
4. **바코드 / QR 스캐너 포트 (Scanner Port)**:
- 각인 후 QR 검증 시 사용되는 바코드/QR 스캐너의 연결 포트를 지정합니다. (`Auto` 또는 특정 `COM` 포트 선택)
- `Auto` 선택 시 입력 수신 단자를 자동으로 자동 인식합니다.
--- ---
## 4.2 설정 저장 ## 4.2 설정 저장
@ -1064,8 +885,22 @@ graph TD
* 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다. * 이 경우 레이저 마킹 장비 구동은 자동으로 제외(Skip)되며, 불합격 내역으로 DB에 기록됩니다.
2. **모든 항목이 합격인 경우 (PASS)**: 2. **모든 항목이 합격인 경우 (PASS)**:
* 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다. * 모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.
* 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다. * 합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.
* 마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다. * 마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.
---
## 7.4 레이저 각인 QR 검증 팝업 및 결과 연동
레이저 마킹이 완료되면 검증을 위해 **QR 검증 팝업 창**이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.
1. **QR 스캔 검증 성공 (즉시 자동 닫힘)**
- 스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 **즉시 자동으로 닫히며** DB에 `OK` 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.
2. **QR 스캔 검증 실패 (NG 판정 및 설명 표출)**
- 스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.
- 팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 **`OK`**에서 **`NG`** (빨간색 테두리 및 배경)로 즉시 전환됩니다.
- 공정 진행 상태 및 시스템 상태 바에 `"마킹 완료 / 각인 QR 검증 NG"`, `"상태: 검사 불합격 (각인 QR 검증 실패)"`, `"각인 QR 검증 실패 (DB NG 저장 완료)"` 등 구체적인 원인 설명이 표시되며, DB에 최종 `NG` 결과로 기록됩니다.
@ -1155,6 +990,12 @@ graph TD
- **순번 범위 초과**: 현재 로트 번호(`Lot No`)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다. - **순번 범위 초과**: 현재 로트 번호(`Lot No`)에서 최종 합격을 받아 각인이 완료된 누적 수량이 9999개를 초과하여 더 이상 순차 번호를 인쇄할 수 없습니다.
- **해결 방법**: 새로운 생산 조 또는 로트 번호(`Lot No`)를 수정하여 **프로그램에 재로그인**하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다. - **해결 방법**: 새로운 생산 조 또는 로트 번호(`Lot No`)를 수정하여 **프로그램에 재로그인**하십시오. 로트 번호가 변경되면 순번 카운트가 다시 초기화됩니다.
### 9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오."
* **현상**: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.
* **원인 및 해결**:
1. **네트워크/DB 서버 점검**: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. `Database.ini` 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.
2. **계정 정보 확인**: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 `LoginOptions.json` 계정 목록에 올바르게 등록되어 있는지 재확인하십시오.
--- ---
## 9.2 프로그램 정상 종료 및 전원 자동 차단 규칙 ## 9.2 프로그램 정상 종료 및 전원 자동 차단 규칙

238
marking_gui/Manual/Manual_for_pdf.html

@ -291,7 +291,7 @@ a:hover {
.toc-list { .toc-list {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 10px; gap: 16px;
} }
.toc-item { .toc-item {
@ -299,24 +299,6 @@ a:hover {
align-items: baseline; align-items: baseline;
font-size: 11pt; font-size: 11pt;
line-height: 1.5; line-height: 1.5;
margin-top: 6px;
}
.toc-sub-item {
display: flex;
align-items: baseline;
font-size: 9.5pt;
line-height: 1.4;
padding-left: 18px;
}
.toc-sub2-item {
display: flex;
align-items: baseline;
font-size: 9pt;
line-height: 1.3;
padding-left: 36px;
color: #6b7280;
} }
.toc-title { .toc-title {
@ -326,29 +308,11 @@ a:hover {
padding-right: 6px; padding-right: 6px;
} }
.toc-sub-item .toc-title {
font-weight: 500;
color: #374151;
}
.toc-sub2-item .toc-title {
font-weight: 400;
color: #4b5563;
}
.toc-title a { .toc-title a {
color: #1f2937 !important; color: #1f2937 !important;
text-decoration: none !important; text-decoration: none !important;
} }
.toc-sub-item .toc-title a {
color: #374151 !important;
}
.toc-sub2-item .toc-title a {
color: #4b5563 !important;
}
.toc-title a:hover { .toc-title a:hover {
color: #007acc !important; color: #007acc !important;
} }
@ -361,14 +325,6 @@ a:hover {
top: -4px; top: -4px;
} }
.toc-sub-item .toc-leader {
border-bottom: 1px dotted #cbd5e1;
}
.toc-sub2-item .toc-leader {
border-bottom: 1px dotted #e2e8f0;
}
.toc-page { .toc-page {
font-weight: 700; font-weight: 700;
color: #374151; color: #374151;
@ -376,16 +332,6 @@ a:hover {
white-space: nowrap; white-space: nowrap;
} }
.toc-sub-item .toc-page {
font-weight: 600;
color: #4b5563;
}
.toc-sub2-item .toc-page {
font-weight: 400;
color: #6b7280;
}
</style> </style>
@ -404,176 +350,46 @@ a:hover {
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">3</span> <span class="toc-page">3</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#11-시스템-개요">1.1 시스템 개요</a></span>
<span class="toc-leader"></span>
<span class="toc-page">3</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#12-소프트웨어-요구사항">1.2 소프트웨어 요구사항</a></span>
<span class="toc-leader"></span>
<span class="toc-page">3</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#13-하드웨어-요구사항">1.3 하드웨어 요구사항</a></span>
<span class="toc-leader"></span>
<span class="toc-page">4</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="01_시스템_개요.md#14-설치-경로-및-파일-배치-구조">1.4 설치 경로 및 파일 배치 구조</a></span>
<span class="toc-leader"></span>
<span class="toc-page">4</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#2-초기-설정-가이드">2. 초기 설정 가이드</a></span> <span class="toc-title"><a href="#2-초기-설정-가이드">2. 초기 설정 가이드</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">5</span> <span class="toc-page">5</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#21-프로그램-설정-구성-개요">2.1 프로그램 설정 구성 개요</a></span>
<span class="toc-leader"></span>
<span class="toc-page">5</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#22-메인-설정-settingini-세부-규격">2.2 메인 설정 (Setting.ini) 세부 규격</a></span>
<span class="toc-leader"></span>
<span class="toc-page">6</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#23-데이터베이스-설정-databaseini">2.3 데이터베이스 설정 (Database.ini)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">7</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="02_초기_설정.md#24-센서-통신-설정-sensorini">2.4 센서 통신 설정 (Sensor.ini)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">7</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#3-메인-화면-home-view">3. 메인 화면 (Home View)</a></span> <span class="toc-title"><a href="#3-메인-화면-home-view">3. 메인 화면 (Home View)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">8</span> <span class="toc-page">8</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="03_메인_화면.md#31-메인-화면-레이아웃-구성">3.1 메인 화면 레이아웃 구성</a></span>
<span class="toc-leader"></span>
<span class="toc-page">8</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="03_메인_화면.md#32-상태-및-제어-카드-영역">3.2 상태 및 제어 카드 영역</a></span>
<span class="toc-leader"></span>
<span class="toc-page">8</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="03_메인_화면.md#33-하단-제어-영역">3.3 하단 제어 영역 (Control Box & Footer)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">10</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#4-통신-설정-comm-settings">4. 통신 설정 (Comm. Settings)</a></span> <span class="toc-title"><a href="#4-통신-설정-comm-settings">4. 통신 설정 (Comm. Settings)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">11</span> <span class="toc-page">11</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="04_통신_설정.md#41-포트-설정-화면-사용법">4.1 포트 설정 화면 사용법</a></span>
<span class="toc-leader"></span>
<span class="toc-page">11</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="04_통신_설정.md#42-설정-저장">4.2 설정 저장</a></span>
<span class="toc-leader"></span>
<span class="toc-page">11</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#5-시험-설정-parameters">5. 시험 설정 (Parameters)</a></span> <span class="toc-title"><a href="#5-시험-설정-parameters">5. 시험 설정 (Parameters)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">12</span> <span class="toc-page">12</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="05_시험_설정.md#51-판정-기준-설정">5.1 판정 기준 설정</a></span>
<span class="toc-leader"></span>
<span class="toc-page">12</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="05_시험_설정.md#52-레이저-각인-도면-지정">5.2 레이저 각인 도면 지정</a></span>
<span class="toc-leader"></span>
<span class="toc-page">13</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="05_시험_설정.md#53-설정-저장-및-적용">5.3 설정 저장 및 적용</a></span>
<span class="toc-leader"></span>
<span class="toc-page">13</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#6-측정-데이터-조회-및-데이터베이스-연동">6. 측정 데이터 조회 (Data View)</a></span> <span class="toc-title"><a href="#6-측정-데이터-조회-및-데이터베이스-연동">6. 측정 데이터 조회 (Data View)</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">14</span> <span class="toc-page">14</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="06_데이터_조회.md#61-선행-공정-결과-조회">6.1 선행 공정 결과 조회 (이력 확인)</a></span>
<span class="toc-leader"></span>
<span class="toc-page">14</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="06_데이터_조회.md#62-검사-결과-자동-적재-및-최종-판정-기록">6.2 검사 결과 자동 적재 및 최종 판정 기록</a></span>
<span class="toc-leader"></span>
<span class="toc-page">15</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="06_데이터_조회.md#63-오프라인-모드-운용">6.3 오프라인 모드 운용</a></span>
<span class="toc-leader"></span>
<span class="toc-page">15</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#7-자동-시험-프로세스-및-14단계-시퀀스">7. 자동 시험 프로세스</a></span> <span class="toc-title"><a href="#7-자동-시험-프로세스-및-14단계-시퀀스">7. 자동 시험 프로세스</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">16</span> <span class="toc-page">16</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="07_시험_프로세스.md#71-자동-시험-및-마킹-프로세스-개요">7.1 자동 시험 및 마킹 프로세스 개요</a></span>
<span class="toc-leader"></span>
<span class="toc-page">16</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="07_시험_프로세스.md#72-상세-15단계-자동-제어-시퀀스">7.2 상세 15단계 자동 제어 시퀀스</a></span>
<span class="toc-leader"></span>
<span class="toc-page">16</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="07_시험_프로세스.md#73-최종-판정-및-마킹-실행-조건">7.3 최종 판정 및 마킹 실행 조건</a></span>
<span class="toc-leader"></span>
<span class="toc-page">17</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#8-로그-및-데이터-관리">8. 로그 및 데이터 관리</a></span> <span class="toc-title"><a href="#8-로그-및-데이터-관리">8. 로그 및 데이터 관리</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">18</span> <span class="toc-page">18</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="08_로그_관리.md#81-시스템-로그-수집-및-관리">8.1 시스템 로그 수집 및 관리</a></span>
<span class="toc-leader"></span>
<span class="toc-page">18</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="08_로그_관리.md#82-장비-교체-및-프로그램-백업-방법">8.2 장비 교체 및 프로그램 백업 방법</a></span>
<span class="toc-leader"></span>
<span class="toc-page">18</span>
</div>
<div class="toc-item"> <div class="toc-item">
<span class="toc-title"><a href="#9-오류-대응-및-프로그램-종료">9. 오류 대응 및 프로그램 종료</a></span> <span class="toc-title"><a href="#9-오류-대응-및-프로그램-종료">9. 오류 대응 및 프로그램 종료</a></span>
<span class="toc-leader"></span> <span class="toc-leader"></span>
<span class="toc-page">19</span> <span class="toc-page">19</span>
</div> </div>
<div class="toc-sub-item">
<span class="toc-title"><a href="09_오류_대응.md#91-현장-장애-유형별-점검-및-예외-조치-요령">9.1 현장 장애 유형별 점검 및 예외 조치 요령</a></span>
<span class="toc-leader"></span>
<span class="toc-page">19</span>
</div>
<div class="toc-sub-item">
<span class="toc-title"><a href="09_오류_대응.md#92-프로그램-정상-종료-및-전원-자동-차단-규칙">9.2 프로그램 정상 종료 및 전원 자동 차단 규칙</a></span>
<span class="toc-leader"></span>
<span class="toc-page">20</span>
</div>
</div> </div>
</div> </div>
@ -633,7 +449,7 @@ a:hover {
</tr> </tr>
<tr> <tr>
<td><strong>레이저 각인 연동</strong></td> <td><strong>레이저 각인 연동</strong></td>
<td>성능 검사가 모두 통과된 합격품에 대해 제품 표면에 2D QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다.</td> <td>성능 검사가 모두 통과된 합격품에 대해 제품 표면에 QR 코드 및 단축 일련번호(생산일자 + 순번)를 마킹합니다.</td>
</tr> </tr>
<tr> <tr>
<td><strong>결과 기록</strong></td> <td><strong>결과 기록</strong></td>
@ -712,7 +528,7 @@ a:hover {
</tr> </tr>
<tr> <tr>
<td><strong>레이저 마킹기</strong></td> <td><strong>레이저 마킹기</strong></td>
<td>2D QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다.</td> <td>QR 코드 및 텍스트 일련번호를 제품 표면에 인쇄합니다.</td>
<td>마킹 제어 보드 연동</td> <td>마킹 제어 보드 연동</td>
</tr> </tr>
</tbody> </tbody>
@ -914,7 +730,8 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<img src="./Pasted image 20260728100343.png" alt="Pasted image 20260728100343.png" class="img-screenshot" /></p> <img src="./Pasted image 20260728100343.png" alt="Pasted image 20260728100343.png" class="img-screenshot" /></p>
<h3>3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단)</h3> <h3>3.2.2 [카드 2] 이전 시험 단계 결과 (좌측 하단)</h3>
<p>생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다. <p>생산 데이터베이스(MS-SQL)를 조회하여 선행 공정의 합격 여부를 판정하는 카드입니다.
* <strong>1. Housing</strong>: 하우징 조립 공정 검사 판정 상태 (<code>PASS</code> / <code>FAIL</code> / <code>대기</code>) * <strong>1. Housing</strong>: 하우징 조립 공정 검사 판정 상태 (<code>PASS</code> / <code>FAIL</code> / <code>대기</code>) 및 서브 텍스트로 <strong><code>PCB Barcode: [값]</code></strong>을 함께 표시합니다.
* <em>주의: Housing 검사가 PASS이더라도 PCB 바코드 값을 읽어오지 못하거나 누락된 경우 Housing 항목은 </em><em>FAIL</em><em>로 처리됩니다.</em>
* <strong>2. Calibration</strong>: 센서 압력 보정 공정 판정 상태 (<code>PASS</code> / <code>FAIL</code> / <code>대기</code>) * <strong>2. Calibration</strong>: 센서 압력 보정 공정 판정 상태 (<code>PASS</code> / <code>FAIL</code> / <code>대기</code>)
* <strong>3. EOL</strong>: EOL 1차 성능 평가 공정 판정 상태 (<code>PASS</code> / <code>FAIL</code> / <code>대기</code>) * <strong>3. EOL</strong>: EOL 1차 성능 평가 공정 판정 상태 (<code>PASS</code> / <code>FAIL</code> / <code>대기</code>)
* <em>주의: 이전 공정 이력 중 하나라도 <code>FAIL</code> 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)</em> * <em>주의: 이전 공정 이력 중 하나라도 <code>FAIL</code> 상태이거나 이력이 없는 경우 메인 성능 검사를 시작할 수 없습니다. (강제 검사 스킵 버튼 미사용 시)</em>
@ -928,12 +745,12 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
<img src="./Pasted image 20260728100404.png" alt="Pasted image 20260728100404.png" class="img-screenshot" /></p> <img src="./Pasted image 20260728100404.png" alt="Pasted image 20260728100404.png" class="img-screenshot" /></p>
<h3>3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)</h3> <h3>3.2.4 [카드 4] 최종 판정 및 종합 상태 영역 (우측 하단)</h3>
<p>진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다. <p>진행 상황 및 최종 판정 결과를 통합 표시하는 영역입니다.
* <strong>💬 진행 상황 뱃지</strong>: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중..." 등 설비 시퀀스 동작을 실시간 텍스트로 표출합니다. * <strong>💬 진행 상황 뱃지</strong>: "대기 상태", "제품 정보 수집 중...", "측정 계측기 데이터 수집 중...", "제품 합격 - 레이저 각인 중...", "각인 QR 검증 실패 (DB NG 저장 완료)" 등 설비 시퀀스 및 검증 동작을 실시간 텍스트로 표출합니다.
* <strong>세부 정보 그리드</strong>: * <strong>세부 정보 그리드</strong>:
- <strong>검사 시간 정보</strong>: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출 - <strong>검사 시간 정보</strong>: 전체 시퀀스 기동 시작 시간 및 종료 시간 표출
- <strong>일련번호 정보</strong>: IC S/N, PCB 바코드, 실제 각인 완료 번호 표출 - <strong>일련번호 정보</strong>: <code>IC S/N</code>, <strong><code>Marking QR :</code></strong> (마킹 생성 QR 값), <strong><code>Scan QR :</code></strong> (스캐너 실측 QR 값), <code>각인 번호</code> 4개 항목을 수직 배열로 표출
- <strong>최종 판정결과</strong>: 최종 <strong>PASS</strong> (초록색) / <strong>FAIL</strong> (빨간색) 대형 배지 표출 - <strong>최종 판정결과</strong>: 최종 <strong>PASS/OK</strong> (초록색) / <strong>FAIL/NG</strong> (빨간색) 대형 배지 표출 (성능 합격 후 마킹 QR 검증 실패 시에도 <strong>NG</strong>로 자동 반영됨)
- <strong>시스템 상태</strong>: 통신 및 디바이스 가동 상태 또는 에러 메시지 배너 표출 - <strong>시스템 상태</strong>: 통신 및 디바이스 가동 상태 또는 에러/검증 실패 사유 메시지 배너 표출
<img src="./Pasted image 20260728100413.png" alt="Pasted image 20260728100413.png" class="img-screenshot" /></p> <img src="./Pasted image 20260728100413.png" alt="Pasted image 20260728100413.png" class="img-screenshot" /></p>
<hr /> <hr />
<h2>3.3 하단 제어 영역 (Control Box &amp; Footer)</h2> <h2>3.3 하단 제어 영역 (Control Box &amp; Footer)</h2>
@ -975,7 +792,14 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
</li> </li>
<li>센서 판독기 내부의 연동 채널 번호를 결정합니다.</li> <li>센서 판독기 내부의 연동 채널 번호를 결정합니다.</li>
<li>드롭다운 메뉴를 통해 <strong><code>1</code></strong> 또는 <strong><code>2</code></strong> 채널을 선택할 수 있습니다.</li> <li>드롭다운 메뉴를 통해 <strong><code>1</code></strong> 또는 <strong><code>2</code></strong> 채널을 선택할 수 있습니다.</li>
<li>선택된 채널 및 포트 정보는 <code>Sensor.ini</code> 파일에 저장됩니다.</li> <li>
<p>선택된 채널 및 포트 정보는 <code>Sensor.ini</code> 파일에 저장됩니다.</p>
</li>
<li>
<p><strong>바코드 / QR 스캐너 포트 (Scanner Port)</strong>:</p>
</li>
<li>각인 후 QR 검증 시 사용되는 바코드/QR 스캐너의 연결 포트를 지정합니다. (<code>Auto</code> 또는 특정 <code>COM</code> 포트 선택)</li>
<li><code>Auto</code> 선택 시 입력 수신 단자를 자동으로 자동 인식합니다.</li>
</ol> </ol>
<hr /> <hr />
<h2>4.2 설정 저장</h2> <h2>4.2 설정 저장</h2>
@ -1144,11 +968,26 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
</li> </li>
<li><strong>모든 항목이 합격인 경우 (PASS)</strong>:<ul> <li><strong>모든 항목이 합격인 경우 (PASS)</strong>:<ul>
<li>모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.</li> <li>모든 수치가 기준치 내에 들어오면 최종 합격(PASS) 판정이 내려집니다.</li>
<li>합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.</li> <li>합격 판정과 동시에 레이저 마킹기에 각인 데이터가 전달되어 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다.</li>
<li>마킹 완료 기록과 계측 수치는 최종 PASS 상태로 데이터베이스에 저장됩니다.</li> <li>마킹 완료 후 QR 스캔 검증 팝업이 활성화되어 각인 상태를 교차 검증합니다.</li>
</ul> </ul>
</li> </li>
</ol> </ol>
<hr />
<h2>7.4 레이저 각인 QR 검증 팝업 및 결과 연동</h2>
<p>레이저 마킹이 완료되면 검증을 위해 <strong>QR 검증 팝업 창</strong>이 화면에 활성화되며 바코드 스캐너 또는 가상 키보드로 각인된 QR 코드를 읽어 들입니다.</p>
<ol>
<li><strong>QR 스캔 검증 성공 (즉시 자동 닫힘)</strong></li>
<li>
<p>스캔된 QR 코드가 예상 QR 코드와 정확히 일치하면 팝업 창이 지연 없이 <strong>즉시 자동으로 닫히며</strong> DB에 <code>OK</code> 상태로 최종 저장을 완료하고 대기 상태로 복구됩니다.</p>
</li>
<li>
<p><strong>QR 스캔 검증 실패 (NG 판정 및 설명 표출)</strong></p>
</li>
<li>스캔된 QR 코드가 불일치하거나 검증을 취소하는 경우, 작업자가 [확인] 버튼을 누르면 팝업 창이 닫힙니다.</li>
<li>팝업이 닫힘과 동시에 메인 화면의 대형 판정 카드가 <strong><code>OK</code></strong>에서 <strong><code>NG</code></strong> (빨간색 테두리 및 배경)로 즉시 전환됩니다.</li>
<li>공정 진행 상태 및 시스템 상태 바에 <code>"마킹 완료 / 각인 QR 검증 NG"</code>, <code>"상태: 검사 불합격 (각인 QR 검증 실패)"</code>, <code>"각인 QR 검증 실패 (DB NG 저장 완료)"</code> 등 구체적인 원인 설명이 표시되며, DB에 최종 <code>NG</code> 결과로 기록됩니다.</li>
</ol>
<div class="page-break"></div> <div class="page-break"></div>
<h1>8. 로그 및 데이터 관리</h1> <h1>8. 로그 및 데이터 관리</h1>
@ -1249,6 +1088,15 @@ Channel=1 ; 통신 채널 번호 (1 또는 2)
</ul> </ul>
</li> </li>
</ul> </ul>
<h3>9.1.6 [오류 6] "DB 연결 상태 또는 ID/PW를 확인해 주십시오."</h3>
<ul>
<li><strong>현상</strong>: 로그인 화면에서 [로그인] 버튼 클릭 시 "DB 연결 상태 또는 ID/PW를 확인해 주십시오." 경고 팝업이 발생하며 로그인되지 않음.</li>
<li><strong>원인 및 해결</strong>:<ol>
<li><strong>네트워크/DB 서버 점검</strong>: 중앙 MSSQL 서버와의 통신이 단선되었거나 서버 전원이 꺼진 경우 타임아웃(1초)에 의해 경고가 표출됩니다. <code>Database.ini</code> 파일의 IP 설정 및 데이터베이스 서버 연결 상태를 점검하십시오.</li>
<li><strong>계정 정보 확인</strong>: 입력한 사용자 ID 및 비밀번호가 DB 계정 테이블 및 <code>LoginOptions.json</code> 계정 목록에 올바르게 등록되어 있는지 재확인하십시오.</li>
</ol>
</li>
</ul>
<hr /> <hr />
<h2>9.2 프로그램 정상 종료 및 전원 자동 차단 규칙</h2> <h2>9.2 프로그램 정상 종료 및 전원 자동 차단 규칙</h2>
<p>본 설비는 고전력 계측 및 전원 제어를 함께 수행하므로 대기 전력 소모 방지와 안전을 위해 아래 절차에 따라 정상 종료 프로세스를 밟아야 합니다.</p> <p>본 설비는 고전력 계측 및 전원 제어를 함께 수행하므로 대기 전력 소모 방지와 안전을 위해 아래 절차에 따라 정상 종료 프로세스를 밟아야 합니다.</p>

2
marking_gui/Models/Product.cs

@ -14,7 +14,7 @@ namespace marking_gui.Models
{ {
get get
{ {
bool housingOk = HousingResult == "PASS" || HousingResult == "OK"; bool housingOk = (HousingResult == "PASS" || HousingResult == "OK") && !string.IsNullOrWhiteSpace(PcbBarcode);
bool calOk = CalResult == "PASS" || CalResult == "OK"; bool calOk = CalResult == "PASS" || CalResult == "OK";
bool eolOk = EolResult == "PASS" || EolResult == "OK"; bool eolOk = EolResult == "PASS" || EolResult == "OK";
return housingOk && calOk && eolOk; return housingOk && calOk && eolOk;

69
marking_gui/PortSettingsWindow.xaml

@ -1,7 +1,7 @@
<Window x:Class="marking_gui.PortSettingsWindow" <Window x:Class="marking_gui.PortSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="포트 설정" Height="280" Width="380" Title="포트 설정" Height="440" Width="420"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#F3F4F6" ShowInTaskbar="False"> Background="#F3F4F6" ShowInTaskbar="False">
<Window.Resources> <Window.Resources>
@ -38,45 +38,78 @@
</Style> </Style>
</Window.Resources> </Window.Resources>
<Grid Margin="20"> <Grid Margin="18">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="*"/> <RowDefinition Height="*"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Text="🔌 4251 센서 통신 포트 설정" FontSize="16" FontWeight="Bold" Foreground="#1F2937" Margin="0,0,0,15"/> <TextBlock Text="포트 통신 설정" FontSize="16" FontWeight="Bold" Foreground="#1F2937" Margin="0,0,0,12"/>
<Grid Grid.Row="1"> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<!-- 1. 4251 센서 포트 설정 -->
<GroupBox Header="4251 센서 통신 포트 설정" FontWeight="Bold" Foreground="#374151" Padding="10" Margin="0,0,0,12">
<Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="10"/> <RowDefinition Height="8"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="10"/> <RowDefinition Height="8"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="130"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 포트 선택 --> <TextBlock Grid.Row="0" Grid.Column="0" Text="통신 포트 (Port):" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock Grid.Row="0" Grid.Column="0" Text="통신 포트 (Port):" FontSize="14" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/> <ComboBox x:Name="cbPort" Grid.Row="0" Grid.Column="1" FontSize="13" Padding="4" Height="28"/>
<ComboBox x:Name="cbPort" Grid.Row="0" Grid.Column="2" FontSize="14" Padding="4" VerticalContentAlignment="Center"/>
<!-- BaudRate 선택 --> <TextBlock Grid.Row="2" Grid.Column="0" Text="통신 속도 (Baud):" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="통신 속도 (Baud):" FontSize="14" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/> <ComboBox x:Name="cbBaudRate" Grid.Row="2" Grid.Column="1" FontSize="13" Padding="4" Height="28"/>
<ComboBox x:Name="cbBaudRate" Grid.Row="2" Grid.Column="2" FontSize="14" Padding="4" VerticalContentAlignment="Center"/>
<!-- 채널 선택 --> <TextBlock Grid.Row="4" Grid.Column="0" Text="통신 채널 (Channel):" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="통신 채널 (Channel):" FontSize="14" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/> <ComboBox x:Name="cbChannel" Grid.Row="4" Grid.Column="1" FontSize="13" Padding="4" Height="28"/>
<ComboBox x:Name="cbChannel" Grid.Row="4" Grid.Column="2" FontSize="14" Padding="4" VerticalContentAlignment="Center"/>
</Grid> </Grid>
</GroupBox>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0"> <!-- 2. 바코드 / QR 스캐너 포트 설정 -->
<GroupBox Header="바코드 / QR 스캐너 포트 설정" FontWeight="Bold" Foreground="#374151" Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="8"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="130"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="스캐너 COM 포트:" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<ComboBox x:Name="cbScannerPort" Grid.Row="0" Grid.Column="1" FontSize="13" Padding="4" Height="28"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="스캐너 통신 속도:" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<ComboBox x:Name="cbScannerBaudRate" Grid.Row="2" Grid.Column="1" FontSize="13" Padding="4" Height="28">
<ComboBoxItem Content="9600"/>
<ComboBoxItem Content="19200"/>
<ComboBoxItem Content="38400"/>
<ComboBoxItem Content="57600"/>
<ComboBoxItem Content="115200"/>
</ComboBox>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="2" Margin="0,15,0,0">
<Button x:Name="btnScanTest" Content="📷 스캔 테스트" Style="{StaticResource DialogButtonStyle}" Background="#4F46E5" Width="120" Height="32" Click="btnScanTest_Click" HorizontalAlignment="Left"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="btnSave" Content="저장" Style="{StaticResource DialogButtonStyle}" Width="70" Height="32" Click="btnSave_Click" Margin="0,0,10,0"/> <Button x:Name="btnSave" Content="저장" Style="{StaticResource DialogButtonStyle}" Width="70" Height="32" Click="btnSave_Click" Margin="0,0,10,0"/>
<Button x:Name="btnCancel" Content="취소" Style="{StaticResource CancelButtonStyle}" Width="70" Height="32" Click="btnCancel_Click"/> <Button x:Name="btnCancel" Content="취소" Style="{StaticResource CancelButtonStyle}" Width="70" Height="32" Click="btnCancel_Click"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Grid>
</Window> </Window>

155
marking_gui/PortSettingsWindow.xaml.cs

@ -1,7 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using System.IO.Ports; using System.IO.Ports;
using System.Linq;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using marking_gui.Services;
namespace marking_gui namespace marking_gui
{ {
@ -10,12 +13,16 @@ namespace marking_gui
/// </summary> /// </summary>
public partial class PortSettingsWindow : Window public partial class PortSettingsWindow : Window
{ {
private string _iniPath; private string _sensorIniPath;
private string _settingIniPath;
public BarcodeScannerSettings ScannerSettings { get; private set; }
public PortSettingsWindow() public PortSettingsWindow()
{ {
InitializeComponent(); InitializeComponent();
_iniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sensor.ini"); _sensorIniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Sensor.ini");
_settingIniPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Setting.ini");
ScannerSettings = BarcodeScannerSettings.Load(_settingIniPath);
Loaded += PortSettingsWindow_Loaded; Loaded += PortSettingsWindow_Loaded;
} }
@ -23,14 +30,20 @@ namespace marking_gui
private void PortSettingsWindow_Loaded(object sender, RoutedEventArgs e) private void PortSettingsWindow_Loaded(object sender, RoutedEventArgs e)
{ {
// 1. 시스템에 유효한 COM 포트 목록 조회 // 1. 시스템에 유효한 COM 포트 목록 조회
string[] ports = SerialPort.GetPortNames(); string[] ports = Array.Empty<string>();
try
{
ports = SerialPort.GetPortNames().OrderBy(p => p).ToArray();
}
catch { }
// ── 4251 센서 포트 설정 바인딩 ──
cbPort.Items.Clear(); cbPort.Items.Clear();
foreach (var port in ports) foreach (var port in ports)
{ {
cbPort.Items.Add(port); cbPort.Items.Add(port);
} }
// 2. BaudRate 목록 추가
cbBaudRate.Items.Clear(); cbBaudRate.Items.Clear();
cbBaudRate.Items.Add("9600"); cbBaudRate.Items.Add("9600");
cbBaudRate.Items.Add("19200"); cbBaudRate.Items.Add("19200");
@ -38,21 +51,19 @@ namespace marking_gui
cbBaudRate.Items.Add("57600"); cbBaudRate.Items.Add("57600");
cbBaudRate.Items.Add("115200"); cbBaudRate.Items.Add("115200");
// 2-2. 채널 목록 추가 (1~4 채널)
cbChannel.Items.Clear(); cbChannel.Items.Clear();
cbChannel.Items.Add("1"); cbChannel.Items.Add("1");
cbChannel.Items.Add("2"); cbChannel.Items.Add("2");
// 3. Sensor.ini 에서 현재 설정 로드
string currentPort = "COM3"; string currentPort = "COM3";
string currentBaud = "115200"; string currentBaud = "115200";
string currentChannel = "1"; string currentChannel = "1";
if (File.Exists(_iniPath)) if (File.Exists(_sensorIniPath))
{ {
try try
{ {
var lines = File.ReadAllLines(_iniPath); var lines = File.ReadAllLines(_sensorIniPath);
foreach (var line in lines) foreach (var line in lines)
{ {
var trimmed = line.Trim(); var trimmed = line.Trim();
@ -75,34 +86,63 @@ namespace marking_gui
} }
catch (Exception ex) catch (Exception ex)
{ {
System.Diagnostics.Debug.WriteLine("[PortSettings] 설정 로딩 중 오류: " + ex.Message); System.Diagnostics.Debug.WriteLine("[PortSettings] 센서 설정 로딩 중 오류: " + ex.Message);
} }
} }
// 4. 조회한 포트 목록에 현재 설정된 포트가 없으면 임시 추가
if (!cbPort.Items.Contains(currentPort)) if (!cbPort.Items.Contains(currentPort))
{ {
cbPort.Items.Add(currentPort); cbPort.Items.Add(currentPort);
} }
// 5. 선택 상태 반영
cbPort.SelectedItem = currentPort; cbPort.SelectedItem = currentPort;
if (cbBaudRate.Items.Contains(currentBaud)) if (cbBaudRate.Items.Contains(currentBaud)) cbBaudRate.SelectedItem = currentBaud;
else cbBaudRate.SelectedItem = "115200";
if (cbChannel.Items.Contains(currentChannel)) cbChannel.SelectedItem = currentChannel;
else cbChannel.SelectedItem = "1";
// ── 바코드 / QR 스캐너 포트 설정 바인딩 ──
if (cbScannerPort != null)
{ {
cbBaudRate.SelectedItem = currentBaud; cbScannerPort.Items.Clear();
cbScannerPort.Items.Add("Auto");
foreach (var port in ports)
{
cbScannerPort.Items.Add(port);
} }
else
string scannerPort = string.IsNullOrWhiteSpace(ScannerSettings.PortName) ? "Auto" : ScannerSettings.PortName.Trim();
bool foundPort = false;
foreach (var item in cbScannerPort.Items)
{ {
cbBaudRate.SelectedItem = "115200"; if (string.Equals(item.ToString(), scannerPort, StringComparison.OrdinalIgnoreCase))
{
cbScannerPort.SelectedItem = item;
foundPort = true;
break;
}
}
if (!foundPort) cbScannerPort.SelectedIndex = 0; // Auto
} }
if (cbChannel.Items.Contains(currentChannel)) if (cbScannerBaudRate != null)
{ {
cbChannel.SelectedItem = currentChannel; string baudStr = ScannerSettings.BaudRate.ToString();
bool foundBaud = false;
foreach (ComboBoxItem item in cbScannerBaudRate.Items)
{
if (string.Equals(item.Content?.ToString(), baudStr, StringComparison.OrdinalIgnoreCase))
{
cbScannerBaudRate.SelectedItem = item;
foundBaud = true;
break;
} }
else }
if (!foundBaud && cbScannerBaudRate.Items.Count > 0)
{ {
cbChannel.SelectedItem = "1"; cbScannerBaudRate.SelectedIndex = 0;
}
} }
} }
@ -110,34 +150,45 @@ namespace marking_gui
{ {
if (cbPort.SelectedItem == null || cbBaudRate.SelectedItem == null || cbChannel.SelectedItem == null) if (cbPort.SelectedItem == null || cbBaudRate.SelectedItem == null || cbChannel.SelectedItem == null)
{ {
MessageBox.Show("포트, 통신 속도 및 채널을 선택해 주십시오.", "알림", MessageBoxButton.OK, MessageBoxImage.Warning); MessageBox.Show("센서 포트, 통신 속도 및 채널을 선택해 주십시오.", "알림", MessageBoxButton.OK, MessageBoxImage.Warning);
return; return;
} }
string port = cbPort.SelectedItem.ToString(); string port = cbPort.SelectedItem.ToString() ?? "COM3";
string baud = cbBaudRate.SelectedItem.ToString(); string baud = cbBaudRate.SelectedItem.ToString() ?? "115200";
string channel = cbChannel.SelectedItem.ToString(); string channel = cbChannel.SelectedItem.ToString() ?? "1";
try try
{ {
// Sensor.ini 에 저장 // 1. Sensor.ini 에 저장
using (var sw = new StreamWriter(_iniPath, false)) using (var sw = new StreamWriter(_sensorIniPath, false))
{ {
sw.WriteLine("; ================================================================="); sw.WriteLine("; =================================================================");
sw.WriteLine("; 성능 검사 모니터링 시스템 - 4251 센서 하드웨어 통신 설정"); sw.WriteLine("; 성능 검사 모니터링 시스템 - 4251 센서 하드웨어 통신 설정");
sw.WriteLine("; ================================================================="); sw.WriteLine("; =================================================================");
sw.WriteLine(); sw.WriteLine();
sw.WriteLine("[Sensor]"); sw.WriteLine("[Sensor]");
sw.WriteLine("; 연결할 장치 센서 시리얼(COM) 포트명");
sw.WriteLine("Port=" + port); sw.WriteLine("Port=" + port);
sw.WriteLine();
sw.WriteLine("; 시리얼 통신 속도 (Baud Rate, 기본값: 115200)");
sw.WriteLine("BaudRate=" + baud); sw.WriteLine("BaudRate=" + baud);
sw.WriteLine();
sw.WriteLine("; 4251 센서 통신 채널 (기본값: 1, 범위: 1~4)");
sw.WriteLine("Channel=" + channel); sw.WriteLine("Channel=" + channel);
} }
// 2. Setting.ini 의 [BarcodeScanner] 섹션에 바코드 스캐너 포트 저장
ScannerSettings.Enabled = true;
if (cbScannerPort != null && cbScannerPort.SelectedItem != null)
{
ScannerSettings.PortName = cbScannerPort.SelectedItem.ToString() ?? "Auto";
}
if (cbScannerBaudRate != null && cbScannerBaudRate.SelectedItem is ComboBoxItem selectedBaudItem)
{
if (int.TryParse(selectedBaudItem.Content?.ToString(), out int scannerBaud))
{
ScannerSettings.BaudRate = scannerBaud;
}
}
ScannerSettings.Save(_settingIniPath);
DialogResult = true; DialogResult = true;
Close(); Close();
} }
@ -147,6 +198,50 @@ namespace marking_gui
} }
} }
private void btnScanTest_Click(object sender, RoutedEventArgs e)
{
string scannerPort = cbScannerPort?.SelectedItem?.ToString() ?? "Auto";
int scannerBaud = 115200;
if (cbScannerBaudRate?.SelectedItem is ComboBoxItem selectedBaudItem && int.TryParse(selectedBaudItem.Content?.ToString(), out int parsedBaud))
{
scannerBaud = parsedBaud;
}
var tempSettings = new BarcodeScannerSettings
{
Enabled = true,
PortName = scannerPort,
BaudRate = scannerBaud
};
SerialBarcodeScanner tempScanner = null;
try
{
tempScanner = new SerialBarcodeScanner(tempSettings);
}
catch { }
string testSampleQr = "TEST_PORT_SCAN;DRAWING_01;HKMC_99;2607299999;";
var qrTestWin = new QrVerificationWindow(testSampleQr, tempScanner)
{
Owner = this,
Title = $"📷 [스캐너 포트 {scannerPort} / {scannerBaud} Baud] 연동 테스트"
};
bool? testResult = qrTestWin.ShowDialog();
try { tempScanner?.Dispose(); } catch { }
if (testResult == true)
{
MessageBox.Show($"스캐너 (포트: {scannerPort}) 통신 및 QR 대조 검증 성공!\n\n현재 선택한 포트로 스캐너가 정상 작동합니다.", "스캔 테스트 성공", MessageBoxButton.OK, MessageBoxImage.Information);
}
else
{
MessageBox.Show($"스캔 테스트가 중단되었거나 취소되었습니다.", "스캔 테스트 종료", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
private void btnCancel_Click(object sender, RoutedEventArgs e) private void btnCancel_Click(object sender, RoutedEventArgs e)
{ {
DialogResult = false; DialogResult = false;

88
marking_gui/QrVerificationWindow.xaml

@ -0,0 +1,88 @@
<Window x:Class="marking_gui.QrVerificationWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="마킹 QR 바코드 검증" Height="520" Width="680"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#F8FAFC" FontFamily="Segoe UI, Malgun Gothic" Loaded="Window_Loaded"
PreviewKeyDown="Window_PreviewKeyDown">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 헤더 영역 -->
<Border Grid.Row="0" Background="#FFFFFF" CornerRadius="8" Padding="16" Margin="0,0,0,16" BorderBrush="#E2E8F0" BorderThickness="1">
<StackPanel>
<TextBlock Text="레이저 각인 QR 바코드 검증" FontSize="18" FontWeight="Bold" Foreground="#0F172A"/>
<TextBlock Text="제품 표면에 각인된 QR 코드를 스캐너로 스캔하십시오." FontSize="13" Foreground="#64748B" Margin="0,4,0,0"/>
</StackPanel>
</Border>
<!-- 데이터 비교 정보 영역 (상단: 예정값, 하단: 스캔값) -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- 1. DB 저장 예정 QR 값 -->
<Border Grid.Row="0" x:Name="borderExpectedCard" Background="#FFFFFF" CornerRadius="8" Padding="16" Margin="0,0,0,12" BorderBrush="#3B82F6" BorderThickness="1.5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row="0">
<TextBlock Text="[DB 저장 예정 QR 값]" FontSize="13" FontWeight="Bold" Foreground="#2563EB"/>
<TextBlock Text="(정답 데이터)" FontSize="11" Foreground="#94A3B8" Margin="8,0,0,0" VerticalAlignment="Center"/>
</DockPanel>
<TextBox Grid.Row="1" x:Name="txtExpectedQr" IsReadOnly="True" BorderThickness="0" Background="Transparent"
FontSize="14" FontWeight="Bold" Foreground="#0F172A" VerticalAlignment="Center" TextWrapping="Wrap"/>
</Grid>
</Border>
<!-- 2. 스캐너로 스캔된 QR 값 -->
<Border Grid.Row="1" x:Name="borderScannedCard" Background="#FFFFFF" CornerRadius="8" Padding="16" Margin="0,0,0,0" BorderBrush="#CBD5E1" BorderThickness="1.5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row="0">
<TextBlock Text="[스캐너 스캔 입력 값]" FontSize="13" FontWeight="Bold" Foreground="#475569"/>
<TextBlock x:Name="txtScanStatusSubText" Text="(스캔 대기 중)" FontSize="11" Foreground="#94A3B8" Margin="8,0,0,0" VerticalAlignment="Center"/>
</DockPanel>
<TextBox Grid.Row="1" x:Name="txtScannedQr" IsReadOnly="True" BorderThickness="0" Background="Transparent"
FontSize="14" FontWeight="Bold" Foreground="#64748B" VerticalAlignment="Center" TextWrapping="Wrap" Text="스캔 대기 중..."/>
</Grid>
</Border>
</Grid>
<!-- 상태 텍스트 배너 -->
<Border Grid.Row="2" x:Name="borderStatusBanner" Background="#F1F5F9" CornerRadius="6" Padding="14" Margin="0,16,0,16">
<TextBlock x:Name="txtStatusMessage" Text="스캔 대기 중" FontSize="16" FontWeight="Bold" Foreground="#334155" HorizontalAlignment="Center"/>
</Border>
<!-- 하단 컨트롤 버튼 (다시스캔 삭제, 취소 및 확인 2개만 깔끔히 배치) -->
<Grid Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="12"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" x:Name="btnCancel" Content="취소 (닫기)" Width="130" Height="42"
Background="#94A3B8" Foreground="#FFFFFF" FontSize="14" FontWeight="Bold"
BorderThickness="0" Cursor="Hand" Click="btnCancel_Click"/>
<Button Grid.Column="3" x:Name="btnConfirm" Content="확인" Width="140" Height="42"
Background="#3B82F6" Foreground="#FFFFFF" FontSize="14" FontWeight="Bold"
BorderThickness="0" Cursor="Hand" Click="btnConfirm_Click"/>
</Grid>
</Grid>
</Window>

284
marking_gui/QrVerificationWindow.xaml.cs

@ -0,0 +1,284 @@
using System;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using marking_gui.Services;
namespace marking_gui
{
public partial class QrVerificationWindow : Window
{
private readonly string _expectedQr;
private readonly SerialBarcodeScanner? _serialScanner;
private bool _isPassed = false;
private bool _hasScanned = false;
private bool _isClosing = false;
private readonly StringBuilder _keyScanBuffer = new StringBuilder();
public string ScannedQr { get; private set; } = string.Empty;
public QrVerificationWindow(string expectedQr, SerialBarcodeScanner? serialScanner = null)
{
InitializeComponent();
_expectedQr = expectedQr ?? string.Empty;
_serialScanner = serialScanner;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
if (txtExpectedQr != null)
{
txtExpectedQr.Text = _expectedQr;
}
if (_serialScanner != null)
{
_serialScanner.ScanReceived += SerialScanner_ScanReceived;
}
ResetToWaitingState();
}
private void Window_Unloaded(object sender, RoutedEventArgs e)
{
if (_serialScanner != null)
{
_serialScanner.ScanReceived -= SerialScanner_ScanReceived;
}
}
private void SerialScanner_ScanReceived(object? sender, BarcodeScanReceivedEventArgs e)
{
Dispatcher.BeginInvoke(new Action(async () =>
{
if (!string.IsNullOrWhiteSpace(e.Text))
{
_serialScanner?.ConfirmPort(e.PortName);
await ProcessScanResultAsync(e.Text.Trim());
}
}), System.Windows.Threading.DispatcherPriority.Input);
}
private void ResetToWaitingState()
{
_hasScanned = false;
_isPassed = false;
_isClosing = false;
_keyScanBuffer.Clear();
if (txtExpectedQr != null)
{
txtExpectedQr.Text = _expectedQr;
}
if (txtScannedQr != null)
{
txtScannedQr.Text = "스캔 대기 중...";
txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(148, 163, 184)); // Gray
}
if (txtScanStatusSubText != null)
{
txtScanStatusSubText.Text = "(스캔 대기 중)";
txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(148, 163, 184));
}
if (borderExpectedCard != null)
{
borderExpectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(59, 130, 246)); // Blue
borderExpectedCard.BorderThickness = new Thickness(1.5);
}
if (borderScannedCard != null)
{
borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(203, 213, 225)); // Gray
borderScannedCard.BorderThickness = new Thickness(1.5);
}
if (borderStatusBanner != null)
{
borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(241, 245, 249)); // Light Gray
}
if (txtStatusMessage != null)
{
txtStatusMessage.Text = "스캔 대기 중";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(51, 65, 85));
}
}
private async void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
// ESC 키로 창 닫기
if (e.Key == Key.Escape)
{
btnCancel_Click(sender, e);
return;
}
// Enter 키 수신 시 입력된 스캔 버퍼 전송
if (e.Key == Key.Enter || e.Key == Key.Return)
{
string scannedText = _keyScanBuffer.ToString().Trim();
_keyScanBuffer.Clear();
if (!string.IsNullOrEmpty(scannedText))
{
await ProcessScanResultAsync(scannedText);
}
return;
}
// 일반 문자 키 수집 (스캐너 가상 키보드 수신)
char keyChar = KeyToChar(e.Key);
if (keyChar != '\0')
{
_keyScanBuffer.Append(keyChar);
}
}
private char KeyToChar(Key key)
{
if (key >= Key.D0 && key <= Key.D9) return (char)('0' + (key - Key.D0));
if (key >= Key.NumPad0 && key <= Key.NumPad9) return (char)('0' + (key - Key.NumPad0));
if (key >= Key.A && key <= Key.Z) return (char)('A' + (key - Key.A));
if (key == Key.OemSemicolon) return ';';
if (key == Key.OemMinus) return '-';
return '\0';
}
private async Task ProcessScanResultAsync(string scannedText)
{
if (_isClosing) return;
_hasScanned = true;
ScannedQr = scannedText ?? string.Empty;
if (txtScannedQr != null)
{
txtScannedQr.Text = scannedText;
}
// nnn;nnnn;nnnn;nnnn; (세미콜론 구분 포맷 정밀 비교)
string normExpected = NormalizeQrFormat(_expectedQr);
string normScanned = NormalizeQrFormat(scannedText);
bool isMatch = string.Equals(normScanned, normExpected, StringComparison.OrdinalIgnoreCase);
if (isMatch)
{
_isPassed = true;
if (txtScannedQr != null)
{
txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Green
}
if (txtScanStatusSubText != null)
{
txtScanStatusSubText.Text = "(스캔 완료 - 일치)";
txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74));
}
if (borderExpectedCard != null)
{
borderExpectedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(34, 197, 94)); // Green
}
if (borderScannedCard != null)
{
borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(34, 197, 94)); // Green
borderScannedCard.BorderThickness = new Thickness(2);
}
if (borderStatusBanner != null)
{
borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(240, 253, 244)); // Light Green
}
if (txtStatusMessage != null)
{
txtStatusMessage.Text = "QR 코드 검증 성공";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(22, 163, 74)); // Dark Green
}
// 스캔값이 합격이면 딜레이 없이 즉시 팝업 닫기
if (!_isClosing)
{
_isClosing = true;
DialogResult = true;
Close();
}
}
else
{
_isPassed = false;
if (txtScannedQr != null)
{
txtScannedQr.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
}
if (txtScanStatusSubText != null)
{
txtScanStatusSubText.Text = "(스캔 완료 - 불일치)";
txtScanStatusSubText.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38));
}
if (borderScannedCard != null)
{
borderScannedCard.BorderBrush = new SolidColorBrush(Color.FromRgb(239, 68, 68)); // Red
borderScannedCard.BorderThickness = new Thickness(2);
}
if (borderStatusBanner != null)
{
borderStatusBanner.Background = new SolidColorBrush(Color.FromRgb(254, 242, 242)); // Light Red
}
if (txtStatusMessage != null)
{
txtStatusMessage.Text = "QR 바코드 불일치";
txtStatusMessage.Foreground = new SolidColorBrush(Color.FromRgb(220, 38, 38)); // Red
}
}
}
private static string NormalizeQrFormat(string input)
{
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
string trimmed = input.Trim();
if (!trimmed.EndsWith(";"))
{
trimmed += ";";
}
return trimmed;
}
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
if (_isClosing) return;
_isClosing = true;
DialogResult = false;
Close();
}
private void btnConfirm_Click(object sender, RoutedEventArgs e)
{
if (_isClosing) return;
if (!_hasScanned)
{
MessageBox.Show("QR 코드를 스캔한 후 확인 버튼을 클릭하십시오.", "스캔 대기", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
_isClosing = true;
DialogResult = _isPassed;
Close();
}
}
}

163
marking_gui/Services/BarcodeScannerSettings.cs

@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace marking_gui.Services
{
public sealed class BarcodeScannerSettings
{
public bool Enabled { get; set; } = true;
public string PortName { get; set; } = "Auto";
public int BaudRate { get; set; } = 9600;
public int ReadTimeout { get; set; } = 500;
public int IdleCommitMilliseconds { get; set; } = 250;
public bool DtrEnable { get; set; } = true;
public bool RtsEnable { get; set; } = true;
public static bool IsAutoPort(string? portName)
{
return string.IsNullOrWhiteSpace(portName) ||
string.Equals(portName.Trim(), "Auto", StringComparison.OrdinalIgnoreCase);
}
public static BarcodeScannerSettings Load(string iniPath)
{
var settings = new BarcodeScannerSettings();
if (!File.Exists(iniPath))
{
return settings;
}
try
{
var lines = File.ReadAllLines(iniPath);
bool isScannerSection = false;
foreach (var line in lines)
{
var trimmed = line.Trim();
if (string.IsNullOrWhiteSpace(trimmed) || trimmed.StartsWith(";") || trimmed.StartsWith("#"))
{
continue;
}
if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
{
string sectionName = trimmed.Substring(1, trimmed.Length - 2).Trim();
isScannerSection = string.Equals(sectionName, "BarcodeScanner", StringComparison.OrdinalIgnoreCase);
continue;
}
if (!isScannerSection)
{
continue;
}
var parts = trimmed.Split(new[] { '=' }, 2);
if (parts.Length != 2)
{
continue;
}
string key = parts[0].Trim();
string val = parts[1].Trim();
if (string.Equals(key, "Enabled", StringComparison.OrdinalIgnoreCase))
{
if (bool.TryParse(val, out bool enabled)) settings.Enabled = enabled;
}
else if (string.Equals(key, "PortName", StringComparison.OrdinalIgnoreCase))
{
settings.PortName = val;
}
else if (string.Equals(key, "BaudRate", StringComparison.OrdinalIgnoreCase))
{
if (int.TryParse(val, out int baudRate)) settings.BaudRate = baudRate;
}
else if (string.Equals(key, "ReadTimeout", StringComparison.OrdinalIgnoreCase))
{
if (int.TryParse(val, out int timeout)) settings.ReadTimeout = timeout;
}
else if (string.Equals(key, "IdleCommitMilliseconds", StringComparison.OrdinalIgnoreCase))
{
if (int.TryParse(val, out int idleMs)) settings.IdleCommitMilliseconds = idleMs;
}
else if (string.Equals(key, "DtrEnable", StringComparison.OrdinalIgnoreCase))
{
if (bool.TryParse(val, out bool dtr)) settings.DtrEnable = dtr;
}
else if (string.Equals(key, "RtsEnable", StringComparison.OrdinalIgnoreCase))
{
if (bool.TryParse(val, out bool rts)) settings.RtsEnable = rts;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[BarcodeScannerSettings Load Error] " + ex.Message);
}
return settings;
}
public void Save(string iniPath)
{
try
{
var lines = File.Exists(iniPath) ? new List<string>(File.ReadAllLines(iniPath)) : new List<string>();
int sectionIndex = -1;
int nextSectionIndex = lines.Count;
for (int i = 0; i < lines.Count; i++)
{
string trimmed = lines[i].Trim();
if (trimmed.StartsWith("[") && trimmed.EndsWith("]"))
{
string sec = trimmed.Substring(1, trimmed.Length - 2).Trim();
if (string.Equals(sec, "BarcodeScanner", StringComparison.OrdinalIgnoreCase))
{
sectionIndex = i;
}
else if (sectionIndex >= 0 && nextSectionIndex == lines.Count)
{
nextSectionIndex = i;
}
}
}
var newSectionLines = new List<string>
{
"[BarcodeScanner]",
$"Enabled={Enabled.ToString().ToLower()}",
$"PortName={PortName}",
$"BaudRate={BaudRate}",
$"ReadTimeout={ReadTimeout}",
$"IdleCommitMilliseconds={IdleCommitMilliseconds}",
$"DtrEnable={DtrEnable.ToString().ToLower()}",
$"RtsEnable={RtsEnable.ToString().ToLower()}"
};
if (sectionIndex >= 0)
{
lines.RemoveRange(sectionIndex, nextSectionIndex - sectionIndex);
lines.InsertRange(sectionIndex, newSectionLines);
}
else
{
if (lines.Count > 0 && !string.IsNullOrWhiteSpace(lines[lines.Count - 1]))
{
lines.Add("");
}
lines.AddRange(newSectionLines);
}
File.WriteAllLines(iniPath, lines);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[BarcodeScannerSettings Save Error] " + ex.Message);
}
}
}
}

69
marking_gui/Services/DatabaseService.cs

@ -120,13 +120,13 @@ namespace marking_gui.Services
_connectionString += "TrustServerCertificate=True;"; _connectionString += "TrustServerCertificate=True;";
} }
// DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 3초로 강제 조정 // DB 서버 오프라인 시 지연 방지를 위해 연결 타임아웃(Connection Timeout)을 1초로 강제 조정하고 재시도 비활성화
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=3;"; _connectionString += "Connect Timeout=1;Connect Retry Count=0;";
} }
} }
catch (Exception ex) catch (Exception ex)
@ -222,6 +222,38 @@ namespace marking_gui.Services
return null; return null;
} }
/// <summary>
/// [dbo].[Marking] 테이블에 해당 IC S/N 데이터가 이미 존재하는지 중복 검사를 수행합니다.
/// </summary>
public bool IsProductExists(string serial)
{
if (_offlineMode || string.IsNullOrWhiteSpace(serial))
{
return false;
}
try
{
string query = "SELECT COUNT(1) FROM [dbo].[Marking] WHERE [IC_SN] = @Serial;";
using (var conn = new System.Data.SqlClient.SqlConnection(_connectionString))
{
using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
{
cmd.CommandTimeout = 3;
cmd.Parameters.AddWithValue("@Serial", serial.Trim());
conn.Open();
int count = (int)cmd.ExecuteScalar();
return count > 0;
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine("[DB IsProductExists] 중복 조회 중 예외: " + ex.Message);
return false;
}
}
/// <summary> /// <summary>
/// 최종 성능 검출 값을 데이터베이스에 저장합니다. /// 최종 성능 검출 값을 데이터베이스에 저장합니다.
/// 전압과 전류 수치는 3,2 포맷(DECIMAL(3, 2) 등)에 대응하기 위해 소수점 이하 2자리로 반올림하여 처리합니다. /// 전압과 전류 수치는 3,2 포맷(DECIMAL(3, 2) 등)에 대응하기 위해 소수점 이하 2자리로 반올림하여 처리합니다.
@ -261,13 +293,7 @@ namespace marking_gui.Services
{ {
serial = serial.Substring(0, 30); serial = serial.Substring(0, 30);
} }
// PCB_Barcode: Housing_Assembly 테이블에서 가져온 값 우선 사용, 없으면 IC_SN으로 대체
string safePcbBarcode = !string.IsNullOrWhiteSpace(pcbBarcode) ? pcbBarcode : serial;
if (safePcbBarcode.Length > 30) safePcbBarcode = safePcbBarcode.Substring(0, 30);
string safeProcessLabel = "Marking";
string safeProcessLabelScan = string.IsNullOrEmpty(result) ? "OK" : (result.Length > 5 ? result.Substring(0, 5) : result);
string safeResult = string.IsNullOrEmpty(result) ? "NG" : (result.Length > 5 ? result.Substring(0, 5) : result); string safeResult = string.IsNullOrEmpty(result) ? "NG" : (result.Length > 5 ? result.Substring(0, 5) : result);
string safeErrorCode = (safeResult == "OK" || safeResult == "PASS") ? "0" : "E01";
// 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);
@ -322,24 +348,23 @@ namespace marking_gui.Services
[Production_Date] = @ProdDate, [Production_Date] = @ProdDate,
[Line] = @Line, [Line] = @Line,
[Lot_No] = @LotNo, [Lot_No] = @LotNo,
[JIG_No] = @JigNo, [Jig_No] = @JigNo,
[PCB_Barcode] = @PcbBarcode,
[Process_Label] = @ProcessLabel,
[Process_Label_Scan] = @ProcessLabelScan,
[PT_Vol_1] = @Vol1, [PT_Vol_1] = @Vol1,
[PT_Current_1] = @Cur1, [PT_Current_1] = @Cur1,
[PT_Resistance_1] = @Res1,
[Marking_QR] = @Qr, [Marking_QR] = @Qr,
[Result] = @Result, [Result] = @Result
[Error_Code] = @ErrorCode
WHEN NOT MATCHED THEN WHEN NOT MATCHED THEN
INSERT ([Maker], [Model], [Variant_1], [Variant_2], [Operator], [Production_Date], [Line], [Lot_No], [JIG_No], [IC_SN], [PCB_Barcode], [Process_Label], [Process_Label_Scan], [PT_Vol_1], [PT_Current_1], [Marking_QR], [Result], [Error_Code]) 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])
VALUES (@Maker, @Model, @Var1, @Var2, @Operator, @ProdDate, @Line, @LotNo, @JigNo, @Serial, @PcbBarcode, @ProcessLabel, @ProcessLabelScan, @Vol1, @Cur1, @Qr, @Result, @ErrorCode);"; VALUES (@Serial, @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))
{ {
using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn)) using (var cmd = new System.Data.SqlClient.SqlCommand(query, conn))
{ {
cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초 cmd.CommandTimeout = 5; // DB 응답 대기 최대 5초
cmd.Parameters.AddWithValue("@Serial", serial);
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);
cmd.Parameters.AddWithValue("@Var1", safeVariant1); cmd.Parameters.AddWithValue("@Var1", safeVariant1);
@ -349,17 +374,11 @@ namespace marking_gui.Services
cmd.Parameters.AddWithValue("@Line", safeLine); cmd.Parameters.AddWithValue("@Line", safeLine);
cmd.Parameters.AddWithValue("@LotNo", safeLotNo); cmd.Parameters.AddWithValue("@LotNo", safeLotNo);
cmd.Parameters.AddWithValue("@JigNo", safeJigNo); cmd.Parameters.AddWithValue("@JigNo", safeJigNo);
cmd.Parameters.AddWithValue("@Serial", serial);
cmd.Parameters.AddWithValue("@PcbBarcode", safePcbBarcode);
cmd.Parameters.AddWithValue("@ProcessLabel", safeProcessLabel);
cmd.Parameters.AddWithValue("@ProcessLabelScan", safeProcessLabelScan);
cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Vol1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 2, Value = roundedVoltage });
cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Cur1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 2, Value = roundedCurrent });
cmd.Parameters.AddWithValue("@Qr", markingQr); cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Vol1", System.Data.SqlDbType.Decimal) { Precision = 4, Scale = 2, Value = roundedVoltage });
cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Cur1", System.Data.SqlDbType.Decimal) { Precision = 4, Scale = 2, Value = roundedCurrent });
cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter("@Res1", System.Data.SqlDbType.Decimal) { Precision = 5, Scale = 3, Value = roundedResistance });
cmd.Parameters.AddWithValue("@Result", safeResult); cmd.Parameters.AddWithValue("@Result", safeResult);
cmd.Parameters.AddWithValue("@ErrorCode", safeErrorCode);
conn.Open(); conn.Open();
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();

41
marking_gui/Services/LoginServicesStub.cs

@ -44,7 +44,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=3;"; connStr += "Connect Timeout=1;Connect Retry Count=0;";
} }
return connStr; return connStr;
@ -338,10 +338,10 @@ namespace Housing.Services
return true; return true;
} }
// DB 커넥션 스트링이 비어있으면 기본값 확인 // DB 커넥션 스트링이 비어있으면 로그인 불가
if (string.IsNullOrWhiteSpace(_dbSettings.ConnectionString)) if (string.IsNullOrWhiteSpace(_dbSettings.ConnectionString))
{ {
return IsDefaultAccount(operatorId, password); return false;
} }
try try
@ -399,42 +399,15 @@ namespace Housing.Services
} }
catch (Exception ex) catch (Exception ex)
{ {
// DB 연결이나 쿼리 오류 발생 시 로그 출력 후 디폴트 계정 검증으로 fallback // DB 연결이나 쿼리 오류 발생 시 상위로 예외 전달 (DB 연결 상태 확인 안내 팝업 표출을 위함)
System.Diagnostics.Debug.WriteLine($"로그인 검증 중 에러 발생: {ex.Message}"); System.Diagnostics.Debug.WriteLine($"로그인 DB 검증 중 에러 발생: {ex.Message}");
throw;
// ==================== [DEMO MODE START] ====================
// DB가 없고 데모 환경일 때도 프로시저를 호출하려는 동작 및 로그 연출을 수행한 후 강제로 성공 처리
// 상용 릴리즈 시 이 블록은 완전히 제거(삭제)할 수 있습니다.
if (!string.IsNullOrWhiteSpace(_loginSettings.Procedure))
{
System.Diagnostics.Debug.WriteLine(string.Format("[데모 시뮬레이션] 프로시저 '{0}' 호출 모사 (ID: {1}, PW: {2}, Process: {3})",
_loginSettings.Procedure, operatorId, password, _loginSettings.ProcessName));
return IsDefaultAccount(operatorId, password);
}
// ==================== [DEMO MODE END] ====================
return IsDefaultAccount(operatorId, password);
} }
} }
private bool IsDefaultAccount(string operatorId, string password) private bool IsDefaultAccount(string operatorId, string password)
{ {
// 기본 개발/테스트용 계정 처리 // 하드코딩 테스트 계정 완전 제거 (LoginOptions.json 및 DB 계정만 사용)
if (string.Equals(operatorId, "test", StringComparison.OrdinalIgnoreCase) &&
string.Equals(password, "test", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(operatorId, "admin", StringComparison.OrdinalIgnoreCase) &&
string.Equals(password, "admin", StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(operatorId, "su", StringComparison.OrdinalIgnoreCase) &&
string.Equals(password, "su", StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false; return false;
} }
} }

263
marking_gui/Services/SerialBarcodeScanner.cs

@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace marking_gui.Services
{
public class BarcodeScanReceivedEventArgs : EventArgs
{
public string PortName { get; }
public string Text { get; }
public bool IsConfirmedPort { get; }
public BarcodeScanReceivedEventArgs(string portName, string text, bool isConfirmedPort)
{
PortName = portName;
Text = text;
IsConfirmedPort = isConfirmedPort;
}
}
public sealed class SerialBarcodeScanner : IDisposable
{
private sealed class PortSession : IDisposable
{
public SerialPort Port { get; }
public StringBuilder Buffer { get; } = new StringBuilder();
public Timer? Timer { get; set; }
private readonly Action<PortSession> _onIdleCommit;
public PortSession(SerialPort port, Action<PortSession> onIdleCommit)
{
Port = port;
_onIdleCommit = onIdleCommit;
}
public void ResetIdleTimer(int milliseconds)
{
if (Timer == null)
{
Timer = new Timer(_ => _onIdleCommit(this), null, milliseconds, Timeout.Infinite);
}
else
{
Timer.Change(milliseconds, Timeout.Infinite);
}
}
public void Dispose()
{
Timer?.Dispose();
try
{
if (Port.IsOpen)
{
Port.Close();
}
Port.Dispose();
}
catch { }
}
}
private readonly BarcodeScannerSettings _settings;
private readonly HashSet<string> _excludedPortNames;
private readonly List<PortSession> _sessions = new List<PortSession>();
private string? _confirmedPortName;
private bool _disposed;
public event EventHandler<BarcodeScanReceivedEventArgs>? ScanReceived;
public bool IsRunning => _sessions.Count > 0;
public string? ConfirmedPortName => _confirmedPortName;
public SerialBarcodeScanner(BarcodeScannerSettings settings, IEnumerable<string>? excludedPortNames = null)
{
_settings = settings ?? new BarcodeScannerSettings();
_excludedPortNames = new HashSet<string>(excludedPortNames ?? Array.Empty<string>(), StringComparer.OrdinalIgnoreCase);
}
public void Start()
{
if (!_settings.Enabled) return;
Stop();
var candidatePorts = GetCandidatePortNames();
if (candidatePorts.Length == 0)
{
System.Diagnostics.Debug.WriteLine("[SerialBarcodeScanner] 사용 가능한 COM 포트가 없습니다.");
return;
}
foreach (var portName in candidatePorts)
{
try
{
var port = CreateSerialPort(portName);
var session = new PortSession(port, CommitBufferedScan);
port.DataReceived += SerialPort_DataReceived;
_sessions.Add(session);
port.Open();
port.DiscardInBuffer();
port.DiscardOutBuffer();
System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] 시리얼 스캐너 포트 오픈 성공: {portName}");
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[SerialBarcodeScanner] 포트 '{portName}' 오픈 실패: {ex.Message}");
}
}
}
public void Stop()
{
foreach (var session in _sessions)
{
try
{
session.Port.DataReceived -= SerialPort_DataReceived;
session.Dispose();
}
catch { }
}
_sessions.Clear();
_confirmedPortName = null;
}
public void ConfirmPort(string portName)
{
if (string.IsNullOrWhiteSpace(portName)) return;
string normalized = NormalizePortName(portName);
if (string.Equals(_confirmedPortName, normalized, StringComparison.OrdinalIgnoreCase)) return;
_confirmedPortName = normalized;
// 포트가 확정되면 다른 미사용 테스트 포트 세션 해제
for (int i = _sessions.Count - 1; i >= 0; i--)
{
var session = _sessions[i];
if (!string.Equals(session.Port.PortName, normalized, StringComparison.OrdinalIgnoreCase))
{
try
{
session.Port.DataReceived -= SerialPort_DataReceived;
session.Dispose();
}
catch { }
_sessions.RemoveAt(i);
}
}
}
private SerialPort CreateSerialPort(string portName)
{
return new SerialPort(portName, _settings.BaudRate)
{
Encoding = Encoding.ASCII,
NewLine = "\r\n",
ReadTimeout = _settings.ReadTimeout,
WriteTimeout = _settings.ReadTimeout,
DtrEnable = _settings.DtrEnable,
RtsEnable = _settings.RtsEnable
};
}
private string[] GetCandidatePortNames()
{
if (!BarcodeScannerSettings.IsAutoPort(_settings.PortName))
{
return new[] { NormalizePortName(_settings.PortName) };
}
try
{
return SerialPort.GetPortNames()
.Select(NormalizePortName)
.Where(p => !_excludedPortNames.Contains(p))
.OrderBy(GetPortNumber)
.ToArray();
}
catch
{
return Array.Empty<string>();
}
}
private static string NormalizePortName(string portName)
{
if (string.IsNullOrWhiteSpace(portName)) return string.Empty;
return portName.Trim().ToUpper();
}
private static int GetPortNumber(string portName)
{
if (portName.StartsWith("COM", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(portName.Substring(3), out int num))
{
return num;
}
return int.MaxValue;
}
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (sender is not SerialPort port) return;
var session = _sessions.FirstOrDefault(s => ReferenceEquals(s.Port, port));
if (session == null) return;
try
{
string text = port.ReadExisting();
if (string.IsNullOrEmpty(text)) return;
lock (session.Buffer)
{
session.Buffer.Append(text);
}
session.ResetIdleTimer(_settings.IdleCommitMilliseconds);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[SerialPort_DataReceived Error] {port.PortName}: {ex.Message}");
}
}
private void CommitBufferedScan(PortSession session)
{
string scanText;
lock (session.Buffer)
{
scanText = session.Buffer.ToString();
session.Buffer.Clear();
}
if (string.IsNullOrWhiteSpace(scanText)) return;
scanText = scanText.Trim();
bool isConfirmed = string.Equals(_confirmedPortName, session.Port.PortName, StringComparison.OrdinalIgnoreCase);
ScanReceived?.Invoke(this, new BarcodeScanReceivedEventArgs(session.Port.PortName, scanText, isConfirmed));
}
public void Dispose()
{
if (!_disposed)
{
Stop();
_disposed = true;
}
}
}
}

19
marking_gui/Setting.ini

@ -6,8 +6,8 @@
; 마킹 제어 라이브러리 및 드라이버(EzCad) 폴더 경로 ; 마킹 제어 라이브러리 및 드라이버(EzCad) 폴더 경로
EzdFolderPath=c:\EzCad EzdFolderPath=c:\EzCad
; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출) ; 테스트 모드 여부 (true 시 하단 테스트 마킹, 강제 검사 버튼 노출 반대 false)
test=false test=true
; 도면품번 (EZD 도안의 도면품번 태그에 채워질 값 — DrawingNoTag에 매핑) ; 도면품번 (EZD 도안의 도면품번 태그에 채워질 값 — DrawingNoTag에 매핑)
DrawingNo=APTSL04A DrawingNo=APTSL04A
@ -111,3 +111,18 @@ MaxCurrent=10.00
MinResistance=8.000 MinResistance=8.000
MaxResistance=13.000 MaxResistance=13.000
; =================================================================
; 바코드 / QR 스캐너 연동 설정 (RS232 / USB Virtual COM / Auto)
; =================================================================
[BarcodeScanner]
Enabled=true
PortName=Auto
BaudRate=9600
ReadTimeout=500
IdleCommitMilliseconds=250
DtrEnable=true
RtsEnable=true

45
marking_gui/SettingsWindow.xaml

@ -1,7 +1,7 @@
<Window x:Class="marking_gui.SettingsWindow" <Window x:Class="marking_gui.SettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="기준 범위 설정" Height="290" Width="350" Title="기준 범위 설정" Height="360" Width="380"
Icon="pack://application:,,,/Marking.ico" Icon="pack://application:,,,/Marking.ico"
WindowStartupLocation="CenterOwner" ResizeMode="NoResize" WindowStartupLocation="CenterOwner" ResizeMode="NoResize"
Background="#F3F4F6" ShowInTaskbar="False"> Background="#F3F4F6" ShowInTaskbar="False">
@ -46,41 +46,48 @@
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Text="🔧 검사 판정 기준 범위 변경" FontSize="16" FontWeight="Bold" Foreground="#1F2937" Margin="0,0,0,15"/> <TextBlock Text="🔧 검사 판정 기준 및 스캐너 설정" FontSize="16" FontWeight="Bold" Foreground="#1F2937" Margin="0,0,0,15"/>
<Grid Grid.Row="1"> <ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<!-- 1. 검사 판정 기준 범위 카드 -->
<GroupBox Header="판정 임계치 기준 설정" FontWeight="Bold" Foreground="#374151" Margin="0,0,0,15" Padding="10">
<Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="10"/> <RowDefinition Height="8"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="10"/> <RowDefinition Height="8"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/> <ColumnDefinition Width="70"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/> <ColumnDefinition Width="16"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- 전압 설정 --> <!-- 전압 설정 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="전압 (V):" FontSize="14" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/> <TextBlock Grid.Row="0" Grid.Column="0" Text="전압 (V):" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBox x:Name="txtMinVoltage" Grid.Row="0" Grid.Column="1" FontSize="14" Padding="4" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/> <TextBox x:Name="txtMinVoltage" Grid.Row="0" Grid.Column="1" FontSize="13" Padding="3" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="~" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBlock Grid.Row="0" Grid.Column="2" Text="~" FontSize="13" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBox x:Name="txtMaxVoltage" Grid.Row="0" Grid.Column="3" FontSize="14" Padding="4" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/> <TextBox x:Name="txtMaxVoltage" Grid.Row="0" Grid.Column="3" FontSize="13" Padding="3" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/>
<!-- 전류 설정 --> <!-- 전류 설정 -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="전류 (A):" FontSize="14" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/> <TextBlock Grid.Row="2" Grid.Column="0" Text="전류 (A):" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBox x:Name="txtMinCurrent" Grid.Row="2" Grid.Column="1" FontSize="14" Padding="4" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/> <TextBox x:Name="txtMinCurrent" Grid.Row="2" Grid.Column="1" FontSize="13" Padding="3" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/>
<TextBlock Grid.Row="2" Grid.Column="2" Text="~" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBlock Grid.Row="2" Grid.Column="2" Text="~" FontSize="13" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBox x:Name="txtMaxCurrent" Grid.Row="2" Grid.Column="3" FontSize="14" Padding="4" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/> <TextBox x:Name="txtMaxCurrent" Grid.Row="2" Grid.Column="3" FontSize="13" Padding="3" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/>
<!-- 저항 설정 --> <!-- 저항 설정 -->
<TextBlock Grid.Row="4" Grid.Column="0" Text="저항 (Ω):" FontSize="14" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/> <TextBlock Grid.Row="4" Grid.Column="0" Text="저항 (Ω):" FontSize="13" VerticalAlignment="Center" Foreground="#4B5563" FontWeight="SemiBold"/>
<TextBox x:Name="txtMinResistance" Grid.Row="4" Grid.Column="1" FontSize="14" Padding="4" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/> <TextBox x:Name="txtMinResistance" Grid.Row="4" Grid.Column="1" FontSize="13" Padding="3" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/>
<TextBlock Grid.Row="4" Grid.Column="2" Text="~" FontSize="14" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBlock Grid.Row="4" Grid.Column="2" Text="~" FontSize="13" VerticalAlignment="Center" HorizontalAlignment="Center"/>
<TextBox x:Name="txtMaxResistance" Grid.Row="4" Grid.Column="3" FontSize="14" Padding="4" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/> <TextBox x:Name="txtMaxResistance" Grid.Row="4" Grid.Column="3" FontSize="13" Padding="3" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" BorderBrush="#D1D5DB" BorderThickness="1"/>
</Grid> </Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0"> <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,15,0,0">
<Button x:Name="btnSave" Content="저장" Style="{StaticResource DialogButtonStyle}" Width="70" Height="32" Click="btnSave_Click" Margin="0,0,10,0"/> <Button x:Name="btnSave" Content="저장" Style="{StaticResource DialogButtonStyle}" Width="70" Height="32" Click="btnSave_Click" Margin="0,0,10,0"/>

2
marking_gui/SettingsWindow.xaml.cs

@ -15,7 +15,6 @@ namespace marking_gui
{ {
InitializeComponent(); InitializeComponent();
// 원본의 데이터 무결성을 유지하기 위해 임시 복제하여 사용
Limits = new MeasurementLimits(limits.MinVoltage, limits.MaxVoltage, limits.MinCurrent, limits.MaxCurrent, limits.MinResistance, limits.MaxResistance); Limits = new MeasurementLimits(limits.MinVoltage, limits.MaxVoltage, limits.MinCurrent, limits.MaxCurrent, limits.MinResistance, limits.MaxResistance);
txtMinVoltage.Text = Limits.MinVoltage.ToString("F1"); txtMinVoltage.Text = Limits.MinVoltage.ToString("F1");
@ -50,6 +49,7 @@ namespace marking_gui
} }
Limits = tempLimits; Limits = tempLimits;
DialogResult = true; DialogResult = true;
Close(); Close();
} }

94
marking_gui/StartupLoginWindow.xaml.cs

@ -49,6 +49,13 @@ namespace Housing.Login
MakerComboBox.Focus(); MakerComboBox.Focus();
} }
private enum LoginCheckStatus
{
Success,
InvalidCredentials,
DbTimeoutOrError
}
private async void LogInButton_Click(object sender, RoutedEventArgs e) private async void LogInButton_Click(object sender, RoutedEventArgs e)
{ {
CloseHistoryPopup(); CloseHistoryPopup();
@ -63,15 +70,26 @@ namespace Housing.Login
button.IsEnabled = false; button.IsEnabled = false;
} }
System.Windows.Input.Mouse.OverrideCursor = System.Windows.Input.Cursors.Wait;
try try
{ {
if (!await IsValidLoginAsync(loginResult.Operator, password)) var checkResult = await CheckLoginAsync(loginResult.Operator, password);
if (checkResult == LoginCheckStatus.InvalidCredentials)
{ {
MessageBox.Show(this, "등록되지 않은 ID/PW입니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error); MessageBox.Show(this, "등록되지 않은 ID/PW입니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error);
PasswordBox.Clear(); PasswordBox.Clear();
PasswordBox.Focus(); PasswordBox.Focus();
return; return;
} }
else if (checkResult == LoginCheckStatus.DbTimeoutOrError)
{
MessageBox.Show(this, "DB 연결 상태 또는 ID/PW를 확인해 주십시오.", "로그인 오류", MessageBoxButton.OK, MessageBoxImage.Warning);
PasswordBox.Clear();
PasswordBox.Focus();
return;
}
loginResult.Password = password; // 비밀번호를 결과 객체에도 담아둠 loginResult.Password = password; // 비밀번호를 결과 객체에도 담아둠
Result = loginResult; Result = loginResult;
@ -87,6 +105,7 @@ namespace Housing.Login
} }
finally finally
{ {
System.Windows.Input.Mouse.OverrideCursor = null;
if (sender is Button loginButton) if (sender is Button loginButton)
{ {
loginButton.IsEnabled = true; loginButton.IsEnabled = true;
@ -94,7 +113,7 @@ namespace Housing.Login
} }
} }
private async Task<bool> IsValidLoginAsync(string loginId, string password) private async Task<LoginCheckStatus> CheckLoginAsync(string loginId, string password)
{ {
// 1. LoginOptions.json의 로컬 계정 정보에서 먼저 찾기 // 1. LoginOptions.json의 로컬 계정 정보에서 먼저 찾기
if (_loginOptions.LoginAccounts != null && _loginOptions.LoginAccounts.Count > 0) if (_loginOptions.LoginAccounts != null && _loginOptions.LoginAccounts.Count > 0)
@ -105,22 +124,44 @@ namespace Housing.Login
if (matched) if (matched)
{ {
return true; return LoginCheckStatus.Success;
} }
} }
// 2. 일치하는 계정이 없다면 기존 DB 조회 로직 태우기 // 2. 오프라인 모드일 경우 통과
var loginSettings = LoginSettings.Load(GetDatabaseIniPath()); var loginSettings = LoginSettings.Load(GetDatabaseIniPath());
if (loginSettings.OfflinePreview) if (loginSettings.OfflinePreview)
{ {
return true; return LoginCheckStatus.Success;
} }
// 3. DB 접속 및 계정 조회 (1초 하드 제한)
try
{
var repository = new LoginAccountRepository( var repository = new LoginAccountRepository(
DatabaseSettings.Load(GetDatabaseIniPath()), DatabaseSettings.Load(GetDatabaseIniPath()),
loginSettings); loginSettings);
return await repository.ExistsAsync(loginId, password); var dbCheckTask = repository.ExistsAsync(loginId, password);
var timeoutTask = Task.Delay(1000); // 1초 하드 제한
var completedTask = await Task.WhenAny(dbCheckTask, timeoutTask);
if (completedTask == dbCheckTask)
{
bool isExist = await dbCheckTask;
return isExist ? LoginCheckStatus.Success : LoginCheckStatus.InvalidCredentials;
}
else
{
System.Diagnostics.Debug.WriteLine("[로그인 타임아웃] DB 응답 1초 초과 -> 타임아웃/DB연결안내 팝업 표출");
return LoginCheckStatus.DbTimeoutOrError;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[로그인 오류] DB 연결 예외 발생: {ex.Message}");
return LoginCheckStatus.DbTimeoutOrError;
}
} }
private bool TryReadRequiredFields(out StartupLoginWindowResult result, out string password) private bool TryReadRequiredFields(out StartupLoginWindowResult result, out string password)
@ -293,24 +334,55 @@ namespace Housing.Login
foreach (var value in values) foreach (var value in values)
{ {
var item = new Button var rowGrid = new Grid
{
Width = menuWidth - 2,
Background = textBox.Background
};
rowGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
rowGrid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var selectBtn = new Button
{ {
Content = value, Content = value,
MinWidth = menuWidth,
Padding = new Thickness(10, 6, 10, 6), Padding = new Thickness(10, 6, 10, 6),
BorderThickness = new Thickness(0), BorderThickness = new Thickness(0),
Background = textBox.Background, Background = System.Windows.Media.Brushes.Transparent,
Foreground = textBox.Foreground, Foreground = textBox.Foreground,
HorizontalContentAlignment = HorizontalAlignment.Left HorizontalContentAlignment = HorizontalAlignment.Left
}; };
item.Click += (_, _) => selectBtn.Click += (_, _) =>
{ {
textBox.Text = value; textBox.Text = value;
textBox.CaretIndex = textBox.Text.Length; textBox.CaretIndex = textBox.Text.Length;
CloseHistoryPopup(); CloseHistoryPopup();
FocusControl(nextFocusControl); FocusControl(nextFocusControl);
}; };
stackPanel.Children.Add(item); Grid.SetColumn(selectBtn, 0);
rowGrid.Children.Add(selectBtn);
var deleteBtn = new Button
{
Content = "✕",
Width = 28,
Height = 28,
Padding = new Thickness(0),
BorderThickness = new Thickness(0),
Background = System.Windows.Media.Brushes.Transparent,
Foreground = System.Windows.Media.Brushes.Red,
FontWeight = FontWeights.Bold,
ToolTip = "이력 삭제"
};
deleteBtn.Click += (s, e) =>
{
_loginHistoryStore.Forget(fieldName, value);
_loginHistoryStore.Save();
ShowHistoryMenu(textBox, fieldName, nextFocusControl);
};
Grid.SetColumn(deleteBtn, 1);
rowGrid.Children.Add(deleteBtn);
stackPanel.Children.Add(rowGrid);
} }
popup.Child = new Border popup.Child = new Border

11
program_flow.md

@ -6,11 +6,11 @@
## 1. 시스템 개요 (Introduction) ## 1. 시스템 개요 (Introduction)
본 시스템은 제조 라인의 최종 단계에서 완제품의 3대 전기적 성능(전압, 전류, 저항)을 통합 검사하고, 적합 제품에 한해 표면에 2D QR 코드 및 일련번호를 레이저 마킹하는 자동화 모니터링 프로그램입니다. 본 시스템은 제조 라인의 최종 단계에서 완제품의 3대 전기적 성능(전압, 전류, 저항)을 통합 검사하고, 적합 제품에 한해 표면에 QR 코드 및 일련번호를 레이저 마킹하는 자동화 모니터링 프로그램입니다.
- **선행 공정 교차 검증:** 제품 바코드(S/N)를 수집하여 이전 주요 공정(Housing, Calibration, EOL)을 모두 통과했는지 생산 데이터베이스에서 자동 조회합니다. - **선행 공정 교차 검증:** 제품 바코드(S/N)를 수집하여 이전 주요 공정(Housing, Calibration, EOL)을 모두 통과했는지 생산 데이터베이스에서 자동 조회합니다.
- **실시간 3대 파라미터 계측:** 전원 공급 장치 및 DMM 계측기와 연동하여 전압(V), 전류(mA), 저항(kΩ/Ω) 수치를 실시간 수집 및 반올림 정리합니다. - **실시간 3대 파라미터 계측:** 전원 공급 장치 및 DMM 계측기와 연동하여 전압(V), 전류(mA), 저항(kΩ/Ω) 수치를 실시간 수집 및 반올림 정리합니다.
- **레이저 각인 연동 (LMC1):** 최종 종합 합격(`PASS`) 건에 한하여 2D QR 코드 및 제품 시퀀스 코드를 레이저로 마킹합니다. - **레이저 각인 연동 (LMC1):** 최종 종합 합격(`PASS`) 건에 한하여 QR 코드 및 제품 시퀀스 코드를 레이저로 마킹합니다.
- **규격 파라미터 조율:** 생산 제품 사양 변경 시 최소/최대 임계치(전압, 전류, 저항 범위)를 간편하게 변경 적용할 수 있습니다. - **규격 파라미터 조율:** 생산 제품 사양 변경 시 최소/최대 임계치(전압, 전류, 저항 범위)를 간편하게 변경 적용할 수 있습니다.
--- ---
@ -74,9 +74,12 @@ graph TD
- 사전 전원 체크 ➔ S/N 수집 및 보정 ➔ 주 테스트 전원 인가 ➔ **전압(V), 저항(kΩ/Ω), 전류(mA)** 순차 실측 ➔ 테스트 전원 안전 차단까지 총 15단계 과정이 백그라운드에서 비동기로 자동 진행됩니다. - 사전 전원 체크 ➔ S/N 수집 및 보정 ➔ 주 테스트 전원 인가 ➔ **전압(V), 저항(kΩ/Ω), 전류(mA)** 순차 실측 ➔ 테스트 전원 안전 차단까지 총 15단계 과정이 백그라운드에서 비동기로 자동 진행됩니다.
2. **합부 판정 (PASS / FAIL):** 2. **합부 판정 (PASS / FAIL):**
- 수집된 3개 측정값이 모두 지정된 최소/최대 규격치 이내인 경우 대형 판정 카드가 초록색 **`PASS`**로 변경됩니다. 단 하나라도 벗어나면 빨간색 **`FAIL`**이 출력됩니다. - 수집된 3개 측정값이 모두 지정된 최소/최대 규격치 이내인 경우 대형 판정 카드가 초록색 **`PASS`**로 변경됩니다. 단 하나라도 벗어나면 빨간색 **`FAIL`**이 출력됩니다.
3. **데이터베이스 적재 및 레이저 각인 (LMC1):** 3. **데이터베이스 적재 및 레이저 각인 (LMC1) & QR 검증:**
- 측정값은 소수점 규격에 맞춰 DB에 안전하게 보관됩니다. - 측정값은 소수점 규격에 맞춰 DB에 안전하게 보관됩니다.
- **PASS 합격품에 한하여** LMC1 레이저 각인기를 통해 2D QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다. (FAIL 제품은 마킹을 스킵하여 제품을 보호합니다.) - **PASS 합격품에 한하여** LMC1 레이저 각인기를 통해 QR 코드 및 제품 일련번호 각인이 자동으로 실행됩니다. (FAIL 제품은 마킹을 스킵하여 제품을 보호합니다.)
- **QR 스캔 교차 검증**: 각인 완료 후 QR 검증 팝업이 활성화됩니다.
- **검증 성공**: 스캔값이 일치하면 팝업이 지연 없이 **즉시 자동으로 닫히며** 최종 `OK` 상태로 DB 저장이 완료됩니다.
- **검증 실패/취소**: 팝업 확인 후 닫히면 메인 화면 대형 판정 카드가 즉시 **`NG`** (빨간색)로 전환되고, `"마킹 완료 / 각인 QR 검증 NG"``"상태: 검사 불합격 (각인 QR 검증 실패)"` 설명이 표출되며 DB에 `NG` 결과로 적재됩니다.
--- ---

Loading…
Cancel
Save