버전 업그레이드

This commit is contained in:
2026-06-18 13:38:27 +09:00
parent a48a4b5fe5
commit ba33a78fec
37 changed files with 3355 additions and 1165 deletions

View File

@@ -1,12 +1,14 @@
import json
import os
import sys
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QSize
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QGridLayout,
QPushButton, QLineEdit, QSpinBox, QDoubleSpinBox,
QLabel, QMessageBox, QApplication, QFileDialog,
QDialog, QTabWidget, QFrame,
QDialog, QTabWidget, QFrame, QListWidget, QListWidgetItem,
)
from camera.insight import InSightCamera
@@ -16,6 +18,7 @@ from db.sql_client import SQLClient
from plc.plc_client import PLCClient
from paths import resolve_path, to_project_relative
from utils.path_helper import get_path
from utils.touch_keyboard import show_touch_keyboard, hide_touch_keyboard
from logger import log_action
ADMIN_PASSWORD = "1234"
@@ -225,6 +228,19 @@ class PasswordDialog(QDialog):
# AdminSettingsDialog
# ══════════════════════════════════════════════════════════════════════ #
class _TouchCheckList(QListWidget):
"""행 전체 탭으로 체크 토글 (터치 화면용)."""
def mousePressEvent(self, event):
item = self.itemAt(event.pos())
if item is not None:
item.setCheckState(
Qt.Unchecked if item.checkState() == Qt.Checked else Qt.Checked
)
return
super().mousePressEvent(event)
class AdminSettingsDialog(QDialog):
def __init__(self, settings_page, parent=None):
super().__init__(parent)
@@ -258,6 +274,7 @@ class AdminSettingsDialog(QDialog):
self._tabs.addTab(self._build_tab_cognex(), "코그넥스")
self._tabs.addTab(self._build_tab_basler(), "Basler")
self._tabs.addTab(self._build_tab_db(), "DB")
self._tabs.addTab(self._build_tab_mes(), "MES 제품")
self._tabs.addTab(self._build_tab_ai(), "AI 모델")
self._tabs.addTab(self._build_tab_conveyor(), "컨베이어")
self._tabs.addTab(self._build_tab_plc(), "PLC")
@@ -387,6 +404,74 @@ class AdminSettingsDialog(QDialog):
form.addRow("", self._btn_pair(btn_connect, btn_save))
return self._tab_wrap(g)
# ── 탭 3b — MES 제품 선택 ───────────────────────────────────────── #
def _build_tab_mes(self) -> QWidget:
w = QWidget()
w.setStyleSheet("background:#1a1a1a;")
layout = QVBoxLayout(w)
layout.setContentsMargins(32, 24, 32, 20)
layout.setSpacing(12)
hint = QLabel(
"제품 등록 탭의 'MES 불러오기'에 표시할 제품을 선택합니다.\n"
"DB 연결 후 목록을 불러온 뒤 체크하고 저장하세요."
)
hint.setStyleSheet("color:#888888; font-size:13px; background:transparent;")
hint.setWordWrap(True)
layout.addWidget(hint)
btn_row = QHBoxLayout()
btn_row.setSpacing(8)
btn_load = QPushButton("목록 불러오기")
btn_load.setFixedHeight(42)
btn_load.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
btn_load.clicked.connect(self._on_mes_load)
btn_all = QPushButton("전체 선택")
btn_all.setFixedHeight(42)
btn_all.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
btn_all.clicked.connect(lambda: self._on_mes_set_all(True))
btn_none = QPushButton("전체 해제")
btn_none.setFixedHeight(42)
btn_none.setStyleSheet(_BTN_DLG.replace("min-height:56px", "min-height:42px"))
btn_none.clicked.connect(lambda: self._on_mes_set_all(False))
btn_row.addWidget(btn_load, stretch=2)
btn_row.addWidget(btn_all, stretch=1)
btn_row.addWidget(btn_none, stretch=1)
layout.addLayout(btn_row)
self._mes_list = _TouchCheckList()
self._mes_list.setSelectionMode(QListWidget.NoSelection)
self._mes_list.setMinimumHeight(380)
self._mes_list.setStyleSheet("""
QListWidget {
background:#1a1a1a; border:1px solid #333333;
border-radius:4px; outline:none; font-size:15px;
}
QListWidget::item {
padding:12px 16px; border-bottom:1px solid #2a2a2a;
}
QListWidget::indicator { width:0px; height:0px; }
""")
self._mes_list.itemChanged.connect(self._on_mes_item_changed)
layout.addWidget(self._mes_list, stretch=1)
self._mes_count_lbl = QLabel("선택: 0 / 0")
self._mes_count_lbl.setStyleSheet("color:#888888; font-size:13px; background:transparent;")
layout.addWidget(self._mes_count_lbl)
btn_save = QPushButton("선택 저장")
btn_save.setFixedHeight(56)
btn_save.setStyleSheet(_BTN_DLG_PRIMARY)
btn_save.clicked.connect(self._on_mes_save)
layout.addWidget(btn_save)
return w
# ── 탭 4 — AI 모델 ──────────────────────────────────────────────── #
def _build_tab_ai(self) -> QWidget:
@@ -538,10 +623,60 @@ class AdminSettingsDialog(QDialog):
btn_close.setStyleSheet(_BTN_DLG)
btn_close.clicked.connect(self.accept)
# 터치 키보드 표시/숨김 토글 버튼 (물리 키보드 없는 터치 모니터용)
self._kb_btn = QPushButton("\u2328") # ⌨ 키보드 글리프
self._kb_btn.setCheckable(True)
self._kb_btn.setFixedSize(56, 56)
self._kb_btn.setToolTip("터치 키보드 표시 / 숨기기")
self._kb_btn.setStyleSheet(
"QPushButton {"
" background:#2a2a2a; color:#cccccc; border:1px solid #555555;"
" border-radius:4px; font-size:24px;"
"}"
"QPushButton:hover { background:#333333; color:#ffffff; }"
"QPushButton:checked { background:#1D9E75; color:#ffffff; border:none; }"
)
self._kb_btn.toggled.connect(self._on_toggle_keyboard)
row.addWidget(self._kb_btn)
row.addWidget(btn_save_all, stretch=2)
row.addWidget(btn_close, stretch=1)
return bar
# ── 터치 키보드 토글 ─────────────────────────────────────────────── #
def _on_toggle_keyboard(self, checked: bool):
if checked:
if self._show_touch_keyboard():
log_action("[설정] 터치 키보드 표시")
else:
# 실패 시 토글 상태 원복 (시그널 재발생 방지)
self._kb_btn.blockSignals(True)
self._kb_btn.setChecked(False)
self._kb_btn.blockSignals(False)
QMessageBox.warning(
self, "터치 키보드",
"터치 키보드를 열 수 없습니다.\n"
"Windows 터치 키보드 또는 화상 키보드를 사용할 수 있는지 확인하세요.",
)
else:
self._hide_touch_keyboard()
log_action("[설정] 터치 키보드 숨김")
def _show_touch_keyboard(self) -> bool:
"""Windows 터치 키보드(TabTip)를 ITipInvocation COM으로 표시한다."""
return show_touch_keyboard()
def _hide_touch_keyboard(self):
"""표시 중인 터치 키보드를 숨긴다."""
hide_touch_keyboard()
def done(self, result: int):
# 키보드를 켠 상태에서만 숨김 (Toggle은 반전이라 미사용 시 닫으면 오히려 켜짐)
if self._kb_btn.isChecked():
self._hide_touch_keyboard()
super().done(result)
@staticmethod
def _make_group(title: str):
from PyQt5.QtWidgets import QGroupBox
@@ -688,6 +823,95 @@ class AdminSettingsDialog(QDialog):
self._sp._config.setdefault("db", {}).update(cfg)
QMessageBox.information(self, "저장", "DB 설정이 저장되었습니다.")
# ── MES 제품 탭 슬롯 ─────────────────────────────────────────────── #
def _get_mes_selected_ids(self) -> list:
ids = []
for i in range(self._mes_list.count()):
item = self._mes_list.item(i)
if item.checkState() == Qt.Checked:
ids.append(item.data(Qt.UserRole))
return ids
def _update_mes_count(self):
total = self._mes_list.count()
selected = len(self._get_mes_selected_ids())
self._mes_count_lbl.setText(f"선택: {selected} / {total}")
@staticmethod
def _style_mes_item(item):
name = item.data(Qt.UserRole + 1) or item.text().lstrip("").lstrip(" ")
item.setData(Qt.UserRole + 1, name)
if item.checkState() == Qt.Checked:
item.setText(f"{name}")
item.setBackground(QColor("#666666"))
item.setForeground(QColor("#ffffff"))
else:
item.setText(f" {name}")
item.setBackground(QColor("#1e1e1e"))
item.setForeground(QColor("#888888"))
def _on_mes_item_changed(self, item):
self._style_mes_item(item)
self._update_mes_count()
def _on_mes_set_all(self, checked: bool):
state = Qt.Checked if checked else Qt.Unchecked
self._mes_list.blockSignals(True)
for i in range(self._mes_list.count()):
item = self._mes_list.item(i)
item.setCheckState(state)
self._style_mes_item(item)
self._mes_list.blockSignals(False)
self._update_mes_count()
def _on_mes_load(self):
client = self._sp._db_client
if not client or not client.is_connected():
QMessageBox.warning(
self, "경고",
"DB가 연결되어 있지 않습니다.\n"
"DB 탭에서 먼저 연결해주세요.",
)
return
items = client.get_all_articles()
if not items:
QMessageBox.warning(self, "경고", "조회된 제품이 없습니다.")
return
saved_ids = set(self._sp._config.get("mes", {}).get("selected_article_ids", []))
self._mes_list.blockSignals(True)
self._mes_list.clear()
for item in items:
li = QListWidgetItem(item["article"])
li.setSizeHint(QSize(0, 52))
li.setFlags(li.flags() | Qt.ItemIsUserCheckable)
li.setData(Qt.UserRole, item["article_id"])
li.setData(Qt.UserRole + 1, item["article"])
li.setToolTip(
f"ID: {item['article_id']} | 모델: {item.get('buyer_article_no', '')}"
)
li.setCheckState(
Qt.Checked if item["article_id"] in saved_ids else Qt.Unchecked
)
self._style_mes_item(li)
self._mes_list.addItem(li)
self._mes_list.blockSignals(False)
self._update_mes_count()
log_action(f"[설정] MES 제품 목록 불러오기: {len(items)}")
def _on_mes_save(self):
selected_ids = self._get_mes_selected_ids()
log_action(f"[설정] MES 제품 선택 저장: {len(selected_ids)}")
self._sp._save_config({"mes": {"selected_article_ids": selected_ids}})
self._sp._config.setdefault("mes", {})["selected_article_ids"] = selected_ids
QMessageBox.information(
self, "저장",
f"{len(selected_ids)}개 제품이 MES 불러오기 목록에 저장되었습니다.",
)
# ── AI 탭 슬롯 ───────────────────────────────────────────────────── #
def _on_ai_browse(self):
@@ -778,6 +1002,13 @@ class AdminSettingsDialog(QDialog):
"ip": self._plc_ip.text().strip(),
"port": self._plc_port.value(),
},
"mes": {
"selected_article_ids": (
self._get_mes_selected_ids()
if self._mes_list.count() > 0
else self._sp._config.get("mes", {}).get("selected_article_ids", [])
),
},
}
try:
self._sp._save_config(data)