버전 업그레이드
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# 검사 페이지 — 코그넥스/Basler 영상, 그룹 A/B 설정, Pass/Fail 표시
|
||||
# 검사 페이지 — 코그넥스/Basler 영상, Pass/Fail 표시
|
||||
import time
|
||||
import threading
|
||||
import itertools
|
||||
@@ -8,12 +8,14 @@ from PyQt5.QtCore import Qt, QThread, pyqtSignal, QSize
|
||||
from PyQt5.QtGui import QImage, QPixmap
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QHBoxLayout, QVBoxLayout, QGroupBox,
|
||||
QPushButton, QLabel, QCheckBox, QFrame,
|
||||
QGridLayout, QSizePolicy,
|
||||
QPushButton, QLabel, QFrame,
|
||||
QGridLayout, QSizePolicy, QScrollArea, QScroller, QMessageBox,
|
||||
)
|
||||
|
||||
from logic.inspector import Inspector
|
||||
from logic.group_manager import GroupManager
|
||||
from logic.products import build_patmax_cells, article_label, MAX_PATMAX_SLOTS
|
||||
from db.sql_client import SQLClient
|
||||
from logger import log_inspect_result, log_camera_timing, log_action, log_defect_image
|
||||
|
||||
_DEFECT_COLORS = {
|
||||
@@ -40,33 +42,6 @@ def _draw_detections(frame: np.ndarray, detections: list) -> np.ndarray:
|
||||
)
|
||||
return img
|
||||
|
||||
# 검사 그룹 A/B 선택용 모델 목록
|
||||
_MODELS = [
|
||||
"LOW REF / LX3 / RH",
|
||||
"LOW REF / LX3 / LH",
|
||||
"LOW REF NAS / LX3 / RH",
|
||||
"LOW REF NAS / LX3 / LH",
|
||||
"LOW REF NAS / MX5a 2.0TH / RH",
|
||||
"LOW REF NAS / MX5a 2.0TH / LH",
|
||||
"HIGH REF / LX3 / RH",
|
||||
"HIGH REF / LX3 / LH",
|
||||
"LOW REF NAS 1.5 GEN / CN7 PE / RH",
|
||||
"LOW REF DOM 1.5 GEN / CN7 PE / LH",
|
||||
]
|
||||
|
||||
_MODEL_ID_MAP = {
|
||||
"LOW REF / LX3 / RH": 1,
|
||||
"LOW REF / LX3 / LH": 2,
|
||||
"LOW REF NAS / LX3 / RH": 3,
|
||||
"LOW REF NAS / LX3 / LH": 4,
|
||||
"LOW REF NAS / MX5a 2.0TH / RH": 5,
|
||||
"LOW REF NAS / MX5a 2.0TH / LH": 6,
|
||||
"HIGH REF / LX3 / RH": 7,
|
||||
"HIGH REF / LX3 / LH": 8,
|
||||
"LOW REF NAS 1.5 GEN / CN7 PE / RH": 9,
|
||||
"LOW REF DOM 1.5 GEN / CN7 PE / LH": 10,
|
||||
}
|
||||
|
||||
|
||||
# ================================================================== #
|
||||
# 백그라운드 워커 — 파이프라인 방식
|
||||
@@ -97,6 +72,18 @@ class InspectWorker(QThread):
|
||||
self._stop_flag = False
|
||||
self._pause_flag = False
|
||||
self._seq = itertools.count(1)
|
||||
self._wk_result_ids: set = set()
|
||||
self._wk_check_enabled = False
|
||||
self._allowed_article_ids: set = set()
|
||||
|
||||
def set_work_targets(self, allowed_article_ids: "set | None"):
|
||||
"""WK_Result 작업 대상 ArticleID (정규화된 set). None이면 WK 검사 비활성."""
|
||||
if allowed_article_ids is None:
|
||||
self._wk_check_enabled = False
|
||||
self._allowed_article_ids = set()
|
||||
return
|
||||
self._wk_check_enabled = True
|
||||
self._allowed_article_ids = allowed_article_ids
|
||||
|
||||
# ── 외부 제어 ──────────────────────────────────────────────────── #
|
||||
|
||||
@@ -207,18 +194,25 @@ class InspectWorker(QThread):
|
||||
ct.join(timeout=10.0)
|
||||
log_camera_timing(seq, "cognex_join_done", _ms())
|
||||
|
||||
# ── 모델 판별 ──
|
||||
results = cognex_out.get("results", {})
|
||||
active_names = self._groups.get_active_group()
|
||||
allowed_ids = [_MODEL_ID_MAP[n] for n in active_names if n in _MODEL_ID_MAP]
|
||||
result_info = {
|
||||
# ── 모델 판별 (WK_Result 작업 대상만 허용) ──
|
||||
results = cognex_out.get("results", {})
|
||||
result_info = {
|
||||
"matched": False, "in_allowed": False,
|
||||
"model": None, "score": 0.0,
|
||||
"cognex_pass": False, "status": "인식 불가",
|
||||
}
|
||||
try:
|
||||
if results:
|
||||
result_info = self._inspector.identify_model(results, allowed_ids)
|
||||
if self._wk_check_enabled:
|
||||
result_info = self._inspector.identify_model(
|
||||
results, allowed_article_ids=self._allowed_article_ids,
|
||||
)
|
||||
else:
|
||||
result_info = self._inspector.identify_model(
|
||||
results, allowed_model_ids=self._groups.get_allowed_ids(),
|
||||
)
|
||||
if result_info["matched"] and self._wk_check_enabled:
|
||||
result_info["in_wk_result"] = result_info["in_allowed"]
|
||||
except Exception as e:
|
||||
print(f"[워커 오류] 모델 판별: {e}")
|
||||
|
||||
@@ -258,18 +252,17 @@ class InspectWorker(QThread):
|
||||
|
||||
class InspectPage(QWidget):
|
||||
def __init__(self, insight_cam, basler_cam, detector=None,
|
||||
belt_delay: float = 3.33, parent=None):
|
||||
belt_delay: float = 3.33, db_client=None, config=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self._insight = insight_cam
|
||||
self._basler = basler_cam
|
||||
self._db_client = db_client
|
||||
self._config = config or {}
|
||||
self.detector = detector
|
||||
self._inspector = Inspector()
|
||||
self._groups = GroupManager()
|
||||
|
||||
self._counts = {
|
||||
"A": {"total": 0, "pass": 0, "fail": 0, "unknown": 0},
|
||||
"B": {"total": 0, "pass": 0, "fail": 0, "unknown": 0},
|
||||
}
|
||||
self._counts = {"total": 0, "pass": 0, "fail": 0, "unknown": 0}
|
||||
|
||||
self._matcher = None
|
||||
|
||||
@@ -284,6 +277,7 @@ class InspectPage(QWidget):
|
||||
self._worker.result_ready.connect(self._on_result)
|
||||
|
||||
self._build_ui()
|
||||
self.refresh_wk_results()
|
||||
|
||||
# ================================================================== #
|
||||
# 최상위 레이아웃
|
||||
@@ -347,68 +341,48 @@ class InspectPage(QWidget):
|
||||
layout.setContentsMargins(8, 6, 8, 6)
|
||||
layout.setSpacing(0)
|
||||
|
||||
layout.addWidget(self._build_col_groups(), stretch=5)
|
||||
layout.addWidget(self._build_col_products(), stretch=5)
|
||||
layout.addWidget(_vline())
|
||||
layout.addWidget(self._build_col_controls(), stretch=3)
|
||||
layout.addWidget(_vline())
|
||||
layout.addWidget(self._build_col_counters(), stretch=4)
|
||||
return w
|
||||
|
||||
def _build_col_groups(self) -> QWidget:
|
||||
def _build_col_products(self) -> QWidget:
|
||||
w = QWidget()
|
||||
layout = QVBoxLayout(w)
|
||||
layout.setContentsMargins(4, 4, 8, 4)
|
||||
layout.setSpacing(6)
|
||||
|
||||
checks_row = QHBoxLayout()
|
||||
checks_row.setSpacing(8)
|
||||
checks_row.addWidget(self._build_group_section("A"), stretch=1)
|
||||
checks_row.addWidget(self._build_group_section("B"), stretch=1)
|
||||
layout.addLayout(checks_row, stretch=1)
|
||||
|
||||
self._switch_btn = QPushButton("현재: 그룹 A 활성 → B로 전환")
|
||||
self._switch_btn.setFixedHeight(56)
|
||||
self._switch_btn.setStyleSheet(
|
||||
"background:#1a3a5c; color:#ffffff; border:none; border-radius:4px;"
|
||||
"font-size:14px; font-weight:bold;"
|
||||
)
|
||||
self._switch_btn.clicked.connect(self._on_switch)
|
||||
layout.addWidget(self._switch_btn)
|
||||
return w
|
||||
|
||||
def _build_group_section(self, name: str) -> QGroupBox:
|
||||
active_color = "#4488ff" if name == "A" else "#cc8844"
|
||||
g = QGroupBox(f"그룹 {name} (최대 4종)")
|
||||
g = QGroupBox("검사 대상")
|
||||
self._products_group = g
|
||||
g.setStyleSheet(
|
||||
f"QGroupBox {{ background:#222222; border:1px solid #333333; border-radius:6px;"
|
||||
f" margin-top:12px; padding:6px 4px 4px 4px; }}"
|
||||
f"QGroupBox::title {{ color:{active_color}; subcontrol-origin:margin;"
|
||||
f" left:8px; font-size:13px; font-weight:bold; }}"
|
||||
"QGroupBox { background:#222222; border:1px solid #333333; border-radius:6px;"
|
||||
" margin-top:12px; padding:6px 4px 4px 4px; }"
|
||||
"QGroupBox::title { color:#4488ff; subcontrol-origin:margin;"
|
||||
" left:8px; font-size:13px; font-weight:bold; }"
|
||||
)
|
||||
layout = QVBoxLayout(g)
|
||||
layout.setSpacing(1)
|
||||
layout.setContentsMargins(4, 2, 4, 2)
|
||||
outer = QVBoxLayout(g)
|
||||
outer.setSpacing(0)
|
||||
outer.setContentsMargins(4, 2, 4, 2)
|
||||
|
||||
checks = []
|
||||
for model in _MODELS:
|
||||
cb = QCheckBox(model)
|
||||
cb.setStyleSheet(
|
||||
"QCheckBox { font-size:12px; min-height:24px; color:#cccccc; }"
|
||||
"QCheckBox::indicator { width:16px; height:16px; }"
|
||||
)
|
||||
checks.append(cb)
|
||||
layout.addWidget(cb)
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll.setFrameShape(QFrame.NoFrame)
|
||||
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||
scroll.setStyleSheet("background:transparent;")
|
||||
QScroller.grabGesture(scroll.viewport(), QScroller.LeftMouseButtonGesture)
|
||||
|
||||
if name == "A":
|
||||
self._group_a_checks = checks
|
||||
for cb in checks:
|
||||
cb.clicked.connect(lambda checked, c=cb: self._on_group_changed("A", c, checked))
|
||||
else:
|
||||
self._group_b_checks = checks
|
||||
for cb in checks:
|
||||
cb.clicked.connect(lambda checked, c=cb: self._on_group_changed("B", c, checked))
|
||||
inner = QWidget()
|
||||
inner.setStyleSheet("background:transparent;")
|
||||
self._products_inner_layout = QVBoxLayout(inner)
|
||||
self._products_inner_layout.setSpacing(2)
|
||||
self._products_inner_layout.setContentsMargins(2, 2, 2, 2)
|
||||
|
||||
return g
|
||||
scroll.setWidget(inner)
|
||||
outer.addWidget(scroll)
|
||||
layout.addWidget(g, stretch=1)
|
||||
return w
|
||||
|
||||
def _build_col_controls(self) -> QWidget:
|
||||
w = QWidget()
|
||||
@@ -433,9 +407,9 @@ class InspectPage(QWidget):
|
||||
)
|
||||
self._pause_btn.clicked.connect(self._on_pause)
|
||||
|
||||
self._active_lbl = QLabel("활성 그룹: A")
|
||||
self._active_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._active_lbl.setStyleSheet("font-size:14px; color:#aaaaaa;")
|
||||
self._scope_lbl = QLabel("검사 범위: —")
|
||||
self._scope_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._scope_lbl.setStyleSheet("font-size:14px; color:#aaaaaa;")
|
||||
|
||||
self._model_lbl = QLabel("인식 모델: —")
|
||||
self._model_lbl.setAlignment(Qt.AlignCenter)
|
||||
@@ -446,19 +420,19 @@ class InspectPage(QWidget):
|
||||
f"벨트 딜레이: {self._worker._belt_delay:.2f}s"
|
||||
)
|
||||
self._belt_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._belt_lbl.setStyleSheet("font-size:12px; color:#666666;")
|
||||
self._belt_lbl.setStyleSheet("font-size:13px; color:#999999;")
|
||||
|
||||
self._result_lbl = QLabel("대기 중")
|
||||
self._result_lbl.setAlignment(Qt.AlignCenter)
|
||||
self._result_lbl.setStyleSheet(
|
||||
"font-size:48px; font-weight:bold; background:#2a2a2a;"
|
||||
"color:#666666; border-radius:8px;"
|
||||
"font-size:64px; font-weight:bold; background:#2a2a2a;"
|
||||
"color:#888888; border-radius:8px;"
|
||||
)
|
||||
self._result_lbl.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
|
||||
layout.addWidget(self._start_btn)
|
||||
layout.addWidget(self._pause_btn)
|
||||
layout.addWidget(self._active_lbl)
|
||||
layout.addWidget(self._scope_lbl)
|
||||
layout.addWidget(self._model_lbl)
|
||||
layout.addWidget(self._belt_lbl)
|
||||
layout.addWidget(self._result_lbl, stretch=1)
|
||||
@@ -473,7 +447,7 @@ class InspectPage(QWidget):
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(4)
|
||||
|
||||
for col, text in enumerate(["", "그룹 A", "그룹 B"]):
|
||||
for col, text in enumerate(["", "집계"]):
|
||||
lbl = QLabel(text)
|
||||
lbl.setAlignment(Qt.AlignCenter)
|
||||
lbl.setStyleSheet("font-size:13px; color:#888888; font-weight:bold;")
|
||||
@@ -491,16 +465,14 @@ class InspectPage(QWidget):
|
||||
row_lbl.setStyleSheet(f"font-size:14px; color:{color}; font-weight:bold;")
|
||||
grid.addWidget(row_lbl, r, 0)
|
||||
|
||||
self._cnt_lbls[key] = {}
|
||||
for g_col, group in enumerate(["A", "B"], start=1):
|
||||
lbl = QLabel("0")
|
||||
lbl.setAlignment(Qt.AlignCenter)
|
||||
lbl.setStyleSheet(
|
||||
f"font-size:36px; font-weight:bold; color:{color};"
|
||||
"background:#222222; border-radius:4px; padding:2px 6px;"
|
||||
)
|
||||
grid.addWidget(lbl, r, g_col)
|
||||
self._cnt_lbls[key][group] = lbl
|
||||
lbl = QLabel("0")
|
||||
lbl.setAlignment(Qt.AlignCenter)
|
||||
lbl.setStyleSheet(
|
||||
f"font-size:36px; font-weight:bold; color:{color};"
|
||||
"background:#222222; border-radius:4px; padding:2px 6px;"
|
||||
)
|
||||
grid.addWidget(lbl, r, 1)
|
||||
self._cnt_lbls[key] = lbl
|
||||
|
||||
layout.addLayout(grid)
|
||||
|
||||
@@ -518,24 +490,6 @@ class InspectPage(QWidget):
|
||||
# 슬롯 — UI 이벤트
|
||||
# ================================================================== #
|
||||
|
||||
def _on_group_changed(self, group: str, changed_cb: QCheckBox, is_checked: bool):
|
||||
checks = self._group_a_checks if group == "A" else self._group_b_checks
|
||||
if is_checked and sum(1 for c in checks if c.isChecked()) > GroupManager.MAX_PER_GROUP:
|
||||
changed_cb.setChecked(False)
|
||||
return
|
||||
models = [c.text() for c in checks if c.isChecked()]
|
||||
if group == "A":
|
||||
self._groups.set_group_a(models)
|
||||
else:
|
||||
self._groups.set_group_b(models)
|
||||
|
||||
def _on_switch(self):
|
||||
active = self._groups.switch_group()
|
||||
other = "B" if active == "A" else "A"
|
||||
self._switch_btn.setText(f"현재: 그룹 {active} 활성 → {other}로 전환")
|
||||
self._active_lbl.setText(f"활성 그룹: {active}")
|
||||
print(f"[검사] 그룹 전환 → 활성 그룹 {active}")
|
||||
|
||||
def _on_start(self):
|
||||
if self._worker.isRunning():
|
||||
return
|
||||
@@ -558,11 +512,18 @@ class InspectPage(QWidget):
|
||||
log_action("[검사] 검사 재개")
|
||||
|
||||
def _on_reset(self):
|
||||
reply = QMessageBox.question(
|
||||
self, "카운터 초기화",
|
||||
"검사 카운트를 0으로 초기화할까요?\n"
|
||||
"초기화한 집계는 되돌릴 수 없습니다.",
|
||||
QMessageBox.Yes | QMessageBox.No, QMessageBox.No,
|
||||
)
|
||||
if reply != QMessageBox.Yes:
|
||||
return
|
||||
log_action("[검사] 카운트 리셋")
|
||||
for key in ("total", "pass", "fail", "unknown"):
|
||||
for g in ("A", "B"):
|
||||
self._counts[g][key] = 0
|
||||
self._cnt_lbls[key][g].setText("0")
|
||||
self._counts[key] = 0
|
||||
self._cnt_lbls[key].setText("0")
|
||||
|
||||
def closeEvent(self, event):
|
||||
self._worker.stop()
|
||||
@@ -598,6 +559,106 @@ class InspectPage(QWidget):
|
||||
self._worker.set_belt_delay(delay)
|
||||
self._belt_lbl.setText(f"벨트 딜레이: {delay:.2f}s")
|
||||
|
||||
def update_db(self, db_client):
|
||||
"""MainWindow에서 DB 연결/해제 시 호출."""
|
||||
self._db_client = db_client
|
||||
self.refresh_wk_results()
|
||||
|
||||
def refresh_wk_results(self):
|
||||
"""vi_AI_WK_Result 기준 작업 대상을 UI·PatMax 매핑·워커에 반영."""
|
||||
if not self._db_client or not self._db_client.is_connected():
|
||||
self._inspector.set_pattern_cells({})
|
||||
self._worker.set_work_targets(None)
|
||||
self._update_product_list([], [])
|
||||
self._scope_lbl.setText("검사 범위: DB 미연결")
|
||||
self._products_group.setTitle("검사 대상")
|
||||
print("[검사] WK_Result 비활성 — DB 미연결")
|
||||
return
|
||||
|
||||
mes_selected = self._config.get("mes", {}).get("selected_article_ids")
|
||||
if mes_selected is not None and len(mes_selected) == 0:
|
||||
self._inspector.set_pattern_cells({})
|
||||
self._worker.set_work_targets(set())
|
||||
self._update_product_list([], [])
|
||||
self._scope_lbl.setText("검사 범위: MES 제품 미선택")
|
||||
self._products_group.setTitle("검사 대상 (0종)")
|
||||
return
|
||||
|
||||
if mes_selected is not None:
|
||||
all_items = self._db_client.get_reflector_list_ordered(mes_selected)
|
||||
else:
|
||||
all_items = self._db_client.get_reflector_list()
|
||||
|
||||
self._inspector.set_pattern_cells(build_patmax_cells(all_items))
|
||||
|
||||
active, inactive = self._db_client.split_articles_by_wk(mes_selected)
|
||||
allowed = {SQLClient._norm_id(a["article_id"]) for a in active}
|
||||
|
||||
self._worker.set_work_targets(allowed)
|
||||
self._update_product_list(active, inactive)
|
||||
|
||||
total = len(active) + len(inactive)
|
||||
if active:
|
||||
self._scope_lbl.setText(f"검사 범위: 작업 대상 {len(active)}종")
|
||||
else:
|
||||
self._scope_lbl.setText("검사 범위: 작업 대상 없음")
|
||||
self._products_group.setTitle(
|
||||
f"검사 대상 (작업 {len(active)}종 / 전체 {total}종)"
|
||||
)
|
||||
print(
|
||||
f"[검사] WK_Result 작업 대상: {len(active)}종 (전체 {total}종, "
|
||||
f"PatMax 슬롯 {min(len(all_items), MAX_PATMAX_SLOTS)}개)"
|
||||
)
|
||||
|
||||
def _update_product_list(self, active: list, inactive: list):
|
||||
if not hasattr(self, "_products_inner_layout"):
|
||||
return
|
||||
|
||||
layout = self._products_inner_layout
|
||||
while layout.count():
|
||||
item = layout.takeAt(0)
|
||||
if item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
if active:
|
||||
layout.addWidget(self._make_section_label(
|
||||
f"작업 대상 — WK_Result ({len(active)})", active=True
|
||||
))
|
||||
for i, article in enumerate(active, 1):
|
||||
layout.addWidget(self._make_article_label(article, i, active=True))
|
||||
|
||||
if inactive:
|
||||
layout.addWidget(self._make_section_label(
|
||||
f"기타 ({len(inactive)})", active=False
|
||||
))
|
||||
for i, article in enumerate(inactive, 1):
|
||||
layout.addWidget(self._make_article_label(article, i, active=False))
|
||||
|
||||
if not active and not inactive:
|
||||
layout.addWidget(self._make_section_label("작업 대상 없음", active=False))
|
||||
|
||||
layout.addStretch()
|
||||
|
||||
@staticmethod
|
||||
def _make_section_label(text: str, active: bool) -> QLabel:
|
||||
lbl = QLabel(text)
|
||||
color = "#aaaaaa" if active else "#666666"
|
||||
lbl.setStyleSheet(
|
||||
f"font-size:12px; color:{color}; font-weight:bold;"
|
||||
"padding:6px 4px 2px 4px; background:#2a2a2a;"
|
||||
)
|
||||
return lbl
|
||||
|
||||
@staticmethod
|
||||
def _make_article_label(article: dict, index: int, active: bool) -> QLabel:
|
||||
lbl = QLabel(f"{index:2d}. {article_label(article)}")
|
||||
if active:
|
||||
style = "font-size:13px; min-height:32px; color:#eeeeee; padding:2px 4px;"
|
||||
else:
|
||||
style = "font-size:13px; min-height:32px; color:#555555; padding:2px 4px;"
|
||||
lbl.setStyleSheet(style)
|
||||
return lbl
|
||||
|
||||
# ================================================================== #
|
||||
# 워커 signal 슬롯 (메인 스레드)
|
||||
# ================================================================== #
|
||||
@@ -606,24 +667,21 @@ class InspectPage(QWidget):
|
||||
self._display_basler_image(frame, detections=detections)
|
||||
|
||||
def _on_result(self, data: dict):
|
||||
group = data["group"]
|
||||
matched = data["matched"]
|
||||
result = data["result"]
|
||||
cognex_pass = data["cognex_pass"]
|
||||
basler_pass = data["basler_pass"]
|
||||
result_info = data["result_info"]
|
||||
|
||||
self._model_lbl.setText(result_info["status"])
|
||||
|
||||
self._counts[group]["total"] += 1
|
||||
self._counts["total"] += 1
|
||||
if not matched:
|
||||
self._counts[group]["unknown"] += 1
|
||||
self._counts["unknown"] += 1
|
||||
elif result == "PASS":
|
||||
self._counts[group]["pass"] += 1
|
||||
self._counts["pass"] += 1
|
||||
else:
|
||||
self._counts[group]["fail"] += 1
|
||||
self._counts["fail"] += 1
|
||||
for key in ("total", "pass", "fail", "unknown"):
|
||||
self._cnt_lbls[key][group].setText(str(self._counts[group][key]))
|
||||
self._cnt_lbls[key].setText(str(self._counts[key]))
|
||||
|
||||
if not matched:
|
||||
self._set_result("미인식", "#332200", "#ff9900")
|
||||
@@ -639,7 +697,7 @@ class InspectPage(QWidget):
|
||||
def _set_result(self, text: str, bg: str, fg: str):
|
||||
self._result_lbl.setText(text)
|
||||
self._result_lbl.setStyleSheet(
|
||||
f"font-size:48px; font-weight:bold; background:{bg};"
|
||||
f"font-size:64px; font-weight:bold; background:{bg};"
|
||||
f"color:{fg}; border-radius:8px;"
|
||||
)
|
||||
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QImage, QPixmap
|
||||
from PyQt5.QtGui import QImage, QPixmap, QColor
|
||||
from PyQt5.QtWidgets import (
|
||||
QWidget, QHBoxLayout, QVBoxLayout, QGroupBox,
|
||||
QPushButton, QListWidget, QListWidgetItem, QLabel,
|
||||
QMessageBox, QScrollArea, QFrame,
|
||||
)
|
||||
|
||||
from db.sql_client import SQLClient
|
||||
|
||||
|
||||
_GRP_STYLE = (
|
||||
"QGroupBox {"
|
||||
@@ -20,11 +22,13 @@ _GRP_STYLE = (
|
||||
|
||||
|
||||
class RegisterPage(QWidget):
|
||||
def __init__(self, insight_cam, matcher=None, db_client=None, parent=None):
|
||||
def __init__(self, insight_cam, matcher=None, db_client=None, config=None, parent=None):
|
||||
super().__init__(parent)
|
||||
self._insight = insight_cam
|
||||
self._db_client = db_client
|
||||
self._config = config or {}
|
||||
self._db_items = []
|
||||
self._wk_map = {}
|
||||
self._selected = None
|
||||
self._captured_img = None
|
||||
|
||||
@@ -49,12 +53,8 @@ class RegisterPage(QWidget):
|
||||
layout.setSpacing(10)
|
||||
|
||||
self._btn_mes = QPushButton("MES 불러오기")
|
||||
self._btn_mes.setVisible(False)
|
||||
self._btn_mes.setEnabled(False)
|
||||
self._btn_mes.setFixedHeight(56)
|
||||
self._btn_mes.setToolTip("DB 연결 후 사용 가능")
|
||||
self._btn_mes.setStyleSheet(
|
||||
"background:#444444; color:#777777; border:none; border-radius:4px; font-size:15px;"
|
||||
)
|
||||
self._btn_mes.clicked.connect(self._on_load_from_db)
|
||||
layout.addWidget(self._btn_mes)
|
||||
|
||||
@@ -69,10 +69,8 @@ class RegisterPage(QWidget):
|
||||
border-radius:4px; outline:none; font-size:15px;
|
||||
}
|
||||
QListWidget::item {
|
||||
padding:0px 14px; border-bottom:1px solid #2a2a2a; color:#dddddd;
|
||||
padding:0px 14px; border-bottom:1px solid #2a2a2a;
|
||||
}
|
||||
QListWidget::item:selected { background:#185FA5; color:#ffffff; }
|
||||
QListWidget::item:hover:!selected { background:#2d2d2d; }
|
||||
""")
|
||||
self._list.currentRowChanged.connect(self._on_select)
|
||||
layout.addWidget(self._list, stretch=1)
|
||||
@@ -109,9 +107,19 @@ class RegisterPage(QWidget):
|
||||
self._lbl_name = _info_value("—")
|
||||
self._lbl_model = _info_value("—")
|
||||
self._lbl_type = _info_value("—")
|
||||
self._lbl_wk = _info_value("—")
|
||||
self._lbl_machine_id = _info_value("—")
|
||||
self._lbl_machine = _info_value("—")
|
||||
self._lbl_work_date = _info_value("—")
|
||||
self._lbl_work_time = _info_value("—")
|
||||
layout.addLayout(_info_row("카테고리", self._lbl_name))
|
||||
layout.addLayout(_info_row("모델명", self._lbl_model))
|
||||
layout.addLayout(_info_row("Type", self._lbl_type))
|
||||
layout.addLayout(_info_row("작업상태", self._lbl_wk))
|
||||
layout.addLayout(_info_row("설비 ID", self._lbl_machine_id))
|
||||
layout.addLayout(_info_row("설비명", self._lbl_machine))
|
||||
layout.addLayout(_info_row("작업시작일", self._lbl_work_date))
|
||||
layout.addLayout(_info_row("작업시작", self._lbl_work_time))
|
||||
|
||||
self._arrow_lbl = QLabel("")
|
||||
self._arrow_lbl.setAlignment(Qt.AlignCenter)
|
||||
@@ -157,21 +165,28 @@ class RegisterPage(QWidget):
|
||||
if row < 0:
|
||||
return
|
||||
item = self._list.item(row)
|
||||
if item is None:
|
||||
if item is None or not (item.flags() & Qt.ItemIsSelectable):
|
||||
return
|
||||
|
||||
article_id = item.data(Qt.UserRole)
|
||||
if article_id is None:
|
||||
return
|
||||
|
||||
self._refresh_list_styles(row)
|
||||
|
||||
db_item = next(
|
||||
(x for x in self._db_items if x["article_id"] == article_id), None
|
||||
(x for x in self._db_items
|
||||
if SQLClient._norm_id(x["article_id"]) == SQLClient._norm_id(article_id)),
|
||||
None,
|
||||
)
|
||||
name = item.data(Qt.UserRole + 1) or item.text()
|
||||
in_wk = bool(item.data(Qt.UserRole + 2))
|
||||
r = {
|
||||
"id": article_id,
|
||||
"name": item.text(),
|
||||
"name": name,
|
||||
"model": db_item.get("buyer_article_no", "") if db_item else "",
|
||||
"type": "",
|
||||
"in_wk": in_wk,
|
||||
}
|
||||
|
||||
self._selected = r
|
||||
@@ -181,6 +196,20 @@ class RegisterPage(QWidget):
|
||||
self._lbl_model.setText(r["model"])
|
||||
t = r.get("type", "")
|
||||
self._lbl_type.setText(t if t else "—")
|
||||
if in_wk:
|
||||
self._lbl_wk.setText("작업 대상")
|
||||
self._lbl_wk.setStyleSheet("color:#cccccc; font-size:16px; font-weight:bold;")
|
||||
wk = self._wk_map.get(SQLClient._norm_id(article_id), {})
|
||||
self._lbl_machine_id.setText(SQLClient.format_db_value(wk.get("machine_id")))
|
||||
self._lbl_machine.setText(SQLClient.format_db_value(wk.get("machine")))
|
||||
self._lbl_work_date.setText(SQLClient.format_db_value(wk.get("work_start_date")))
|
||||
self._lbl_work_time.setText(SQLClient.format_db_value(wk.get("work_start_time")))
|
||||
else:
|
||||
self._lbl_wk.setText("작업 대외")
|
||||
self._lbl_wk.setStyleSheet("color:#888888; font-size:16px; font-weight:bold;")
|
||||
for lbl in (self._lbl_machine_id, self._lbl_machine,
|
||||
self._lbl_work_date, self._lbl_work_time):
|
||||
lbl.setText("—")
|
||||
|
||||
if t == "RH":
|
||||
self._arrow_lbl.setText("→")
|
||||
@@ -219,35 +248,111 @@ class RegisterPage(QWidget):
|
||||
def update_db(self, db_client):
|
||||
"""MainWindow에서 DB 연결/해제 시 호출."""
|
||||
self._db_client = db_client
|
||||
enabled = db_client is not None and db_client.is_connected()
|
||||
self._btn_mes.setEnabled(enabled)
|
||||
self._btn_mes.setStyleSheet(
|
||||
"background:#1a3a5c; color:#ffffff; border:none; border-radius:4px; font-size:15px;"
|
||||
if enabled else
|
||||
"background:#444444; color:#777777; border:none; border-radius:4px; font-size:15px;"
|
||||
self.load_products()
|
||||
|
||||
def load_products(self):
|
||||
"""관리자 설정에서 선택한 MES 제품 목록을 자동으로 불러온다."""
|
||||
self._list.clear()
|
||||
self._db_items = []
|
||||
self._wk_map = {}
|
||||
self._selected = None
|
||||
self._lbl_name.setText("—")
|
||||
self._lbl_model.setText("—")
|
||||
self._lbl_type.setText("—")
|
||||
self._lbl_wk.setText("—")
|
||||
self._lbl_wk.setStyleSheet("color:#ffffff; font-size:16px; font-weight:bold;")
|
||||
for lbl in (self._lbl_machine_id, self._lbl_machine,
|
||||
self._lbl_work_date, self._lbl_work_time):
|
||||
lbl.setText("—")
|
||||
self._arrow_lbl.setText("")
|
||||
self._reset_preview()
|
||||
|
||||
if not self._db_client or not self._db_client.is_connected():
|
||||
return
|
||||
|
||||
selected_ids = self._config.get("mes", {}).get("selected_article_ids")
|
||||
if selected_ids is not None and len(selected_ids) == 0:
|
||||
return
|
||||
|
||||
all_items = self._db_client.get_reflector_list(
|
||||
article_ids=selected_ids if selected_ids is not None else None
|
||||
)
|
||||
if not all_items:
|
||||
return
|
||||
|
||||
self._wk_map = self._db_client.get_wk_result_map()
|
||||
active, inactive = self._db_client.split_articles_by_wk(selected_ids)
|
||||
|
||||
self._db_items = all_items
|
||||
|
||||
if active:
|
||||
self._add_section_header(f"작업 대상 — WK_Result ({len(active)})")
|
||||
for item in active:
|
||||
self._add_product_item(item, in_wk=True)
|
||||
|
||||
if inactive:
|
||||
self._add_section_header(f"기타 ({len(inactive)})")
|
||||
for item in inactive:
|
||||
self._add_product_item(item, in_wk=False)
|
||||
|
||||
self._refresh_list_styles()
|
||||
|
||||
def _on_load_from_db(self):
|
||||
if not self._db_client or not self._db_client.is_connected():
|
||||
QMessageBox.warning(self, "경고", "DB를 먼저 연결해주세요.")
|
||||
return
|
||||
pass
|
||||
|
||||
items = self._db_client.get_reflector_list()
|
||||
if not items:
|
||||
QMessageBox.warning(self, "경고", "조회된 제품이 없습니다.")
|
||||
return
|
||||
def _add_section_header(self, text: str):
|
||||
hi = QListWidgetItem(text)
|
||||
hi.setFlags(Qt.NoItemFlags)
|
||||
hi.setForeground(QColor("#aaaaaa"))
|
||||
hi.setBackground(QColor("#2a2a2a"))
|
||||
hi.setSizeHint(QSize(0, 40))
|
||||
self._list.addItem(hi)
|
||||
|
||||
self._list.clear()
|
||||
self._db_items = items
|
||||
for item in items:
|
||||
li = QListWidgetItem(item['article'])
|
||||
li.setSizeHint(QSize(0, 52))
|
||||
li.setData(Qt.UserRole, item["article_id"])
|
||||
self._list.addItem(li)
|
||||
def _add_product_item(self, item: dict, in_wk: bool):
|
||||
li = QListWidgetItem(item["article"])
|
||||
li.setSizeHint(QSize(0, 52))
|
||||
li.setData(Qt.UserRole, item["article_id"])
|
||||
li.setData(Qt.UserRole + 1, item["article"])
|
||||
li.setData(Qt.UserRole + 2, in_wk)
|
||||
if in_wk:
|
||||
wk = self._wk_map.get(SQLClient._norm_id(item["article_id"]), {})
|
||||
tips = []
|
||||
if wk.get("machine_id") is not None:
|
||||
tips.append(f"설비 ID: {SQLClient.format_db_value(wk.get('machine_id'))}")
|
||||
if wk.get("machine") is not None:
|
||||
tips.append(f"설비: {SQLClient.format_db_value(wk.get('machine'))}")
|
||||
if wk.get("work_start_date") is not None:
|
||||
tips.append(f"시작일: {SQLClient.format_db_value(wk.get('work_start_date'))}")
|
||||
if wk.get("work_start_time") is not None:
|
||||
tips.append(f"시작: {SQLClient.format_db_value(wk.get('work_start_time'))}")
|
||||
if tips:
|
||||
li.setToolTip("\n".join(tips))
|
||||
self._list.addItem(li)
|
||||
self._style_product_item(li, in_wk, selected=False)
|
||||
|
||||
QMessageBox.information(
|
||||
self, "완료", f"{len(items)}개 제품을 불러왔습니다."
|
||||
)
|
||||
@staticmethod
|
||||
def _style_product_item(item: QListWidgetItem, in_wk: bool, selected: bool):
|
||||
if in_wk:
|
||||
if selected:
|
||||
bg, fg = "#777777", "#ffffff"
|
||||
else:
|
||||
bg, fg = "#555555", "#eeeeee"
|
||||
elif selected:
|
||||
bg, fg = "#3a3a3a", "#aaaaaa"
|
||||
else:
|
||||
bg, fg = "#1e1e1e", "#666666"
|
||||
item.setBackground(QColor(bg))
|
||||
item.setForeground(QColor(fg))
|
||||
|
||||
def _refresh_list_styles(self, selected_row: int = -1):
|
||||
if selected_row < 0:
|
||||
selected_row = self._list.currentRow()
|
||||
for i in range(self._list.count()):
|
||||
item = self._list.item(i)
|
||||
if not (item.flags() & Qt.ItemIsSelectable):
|
||||
continue
|
||||
in_wk = bool(item.data(Qt.UserRole + 2))
|
||||
self._style_product_item(item, in_wk, selected=(i == selected_row))
|
||||
|
||||
# ================================================================== #
|
||||
# 헬퍼
|
||||
|
||||
@@ -754,7 +754,7 @@ class RetrainPage(QWidget):
|
||||
"color:#aaaaaa; font-size:12px; min-width:55px;"
|
||||
)
|
||||
hint = QLabel("더블클릭: fit | 휠: 줌 | Space+드래그: 패닝 | Del: 박스삭제 | Ctrl+Z: 실행취소")
|
||||
hint.setStyleSheet("color:#555555; font-size:11px;")
|
||||
hint.setStyleSheet("color:#888888; font-size:12px;")
|
||||
btn_fit = QPushButton("초기화")
|
||||
btn_fit.setFixedHeight(28)
|
||||
btn_fit.setStyleSheet(_btn_style("#333333", font_size=12))
|
||||
@@ -1174,6 +1174,12 @@ def _spinbox_style() -> str:
|
||||
" background:#2a2a2a; color:#ffffff; border:1px solid #555555;"
|
||||
" border-radius:4px; padding:4px 8px; font-size:14px; min-height:38px;"
|
||||
"}"
|
||||
"QSpinBox::up-button {"
|
||||
" subcontrol-origin:border; subcontrol-position:top right; width:30px;"
|
||||
"}"
|
||||
"QSpinBox::down-button {"
|
||||
" subcontrol-origin:border; subcontrol-position:bottom right; width:30px;"
|
||||
"}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user