# 제품 등록 페이지 — 기준 이미지 캡처 import cv2 import numpy as np from PyQt5.QtCore import Qt, QSize 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 {" " background:#222222; border:1px solid #333333; border-radius:6px;" " margin-top:14px; padding:14px 12px 12px 12px;" "}" "QGroupBox::title { color:#aaaaaa; subcontrol-origin:margin; left:10px; padding:0 4px; }" ) class RegisterPage(QWidget): 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 self._build_ui() # ================================================================== # # UI 구성 # ================================================================== # def _build_ui(self): root = QHBoxLayout(self) root.setContentsMargins(0, 0, 0, 0) root.setSpacing(0) root.addWidget(self._build_left_panel(), stretch=2) root.addWidget(self._build_right_panel(), stretch=3) def _build_left_panel(self) -> QWidget: w = QWidget() w.setStyleSheet("background:#1a1a1a;") layout = QVBoxLayout(w) layout.setContentsMargins(16, 16, 8, 16) layout.setSpacing(10) self._btn_mes = QPushButton("MES 불러오기") self._btn_mes.setVisible(False) self._btn_mes.setEnabled(False) self._btn_mes.clicked.connect(self._on_load_from_db) layout.addWidget(self._btn_mes) lbl = QLabel("제품 목록") lbl.setStyleSheet("font-size:13px; color:#777777;") layout.addWidget(lbl) self._list = QListWidget() self._list.setStyleSheet(""" QListWidget { background:#222222; border:1px solid #333333; border-radius:4px; outline:none; font-size:15px; } QListWidget::item { padding:0px 14px; border-bottom:1px solid #2a2a2a; } """) self._list.currentRowChanged.connect(self._on_select) layout.addWidget(self._list, stretch=1) return w def _build_right_panel(self) -> QScrollArea: scroll = QScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QScrollArea.NoFrame) scroll.setStyleSheet("background:#1a1a1a;") inner = QWidget() inner.setStyleSheet("background:#1a1a1a;") layout = QVBoxLayout(inner) layout.setContentsMargins(8, 16, 16, 16) layout.setSpacing(0) layout.addWidget(self._build_detail_section()) layout.addWidget(_divider()) layout.addWidget(self._build_capture_section()) layout.addStretch() scroll.setWidget(inner) return scroll # ── 섹션 1: 제품 상세 ────────────────────────────────────────────── def _build_detail_section(self) -> QGroupBox: g = QGroupBox("제품 상세 정보") g.setStyleSheet(_GRP_STYLE) layout = QVBoxLayout(g) layout.setSpacing(8) 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) self._arrow_lbl.setFixedHeight(80) self._arrow_lbl.setStyleSheet("font-size:60px; background:transparent;") layout.addWidget(self._arrow_lbl) return g # ── 섹션 2: 캡처 ────────────────────────────────────────────────── def _build_capture_section(self) -> QGroupBox: g = QGroupBox("기준 이미지 캡처") g.setStyleSheet(_GRP_STYLE) layout = QVBoxLayout(g) layout.setSpacing(12) btn = QPushButton("캡처 (In-Sight 트리거)") btn.setFixedHeight(56) btn.setStyleSheet( "background:#1a3a5c; color:#ffffff; border:none; border-radius:4px;" "font-size:15px; font-weight:bold;" ) btn.clicked.connect(self._on_capture) self._preview = QLabel() self._preview.setAlignment(Qt.AlignCenter) self._preview.setFixedSize(400, 300) self._preview.setStyleSheet( "background:#111111; color:#555555; border:1px solid #333333;" "border-radius:4px; font-size:14px;" ) self._preview.setText("캡처 이미지 없음") layout.addWidget(btn) layout.addWidget(self._preview, alignment=Qt.AlignHCenter) return g # ================================================================== # # 슬롯 # ================================================================== # def _on_select(self, row: int): if row < 0: return item = self._list.item(row) 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 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": name, "model": db_item.get("buyer_article_no", "") if db_item else "", "type": "", "in_wk": in_wk, } self._selected = r self._captured_img = None self._lbl_name.setText(r["name"]) 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("→") self._arrow_lbl.setStyleSheet("font-size:60px; color:#4488ff; background:transparent;") elif t == "LH": self._arrow_lbl.setText("←") self._arrow_lbl.setStyleSheet("font-size:60px; color:#ff8844; background:transparent;") else: self._arrow_lbl.setText("") self._reset_preview() def _on_capture(self): if self._selected is None: QMessageBox.warning(self, "경고", "먼저 제품을 선택하세요.") return if not self._insight.is_connected(): QMessageBox.critical(self, "오류", "Cognex 카메라가 연결되어 있지 않습니다.") return raw = self._insight.trigger_and_get_image() if not raw: QMessageBox.critical(self, "캡처 실패", "이미지를 수신하지 못했습니다.\n카메라 연결 상태를 확인하세요.") return arr = np.frombuffer(raw, dtype=np.uint8) img = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED) if img is None: QMessageBox.critical(self, "캡처 실패", "이미지 디코딩에 실패했습니다.") return self._captured_img = img self._show_ndarray(img) def update_db(self, db_client): """MainWindow에서 DB 연결/해제 시 호출.""" self._db_client = db_client 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): pass 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) 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) @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)) # ================================================================== # # 헬퍼 # ================================================================== # def _show_ndarray(self, img: np.ndarray): h_img, w_img = img.shape[:2] if img.ndim == 2: qimg = QImage(img.data, w_img, h_img, w_img, QImage.Format_Grayscale8) else: rgb = np.ascontiguousarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) qimg = QImage(rgb.data, w_img, h_img, w_img * 3, QImage.Format_RGB888) pix = QPixmap.fromImage(qimg).scaled(400, 300, Qt.KeepAspectRatio, Qt.SmoothTransformation) self._preview.setPixmap(pix) self._preview.setText("") def _reset_preview(self): self._preview.clear() self._preview.setText("캡처 이미지 없음") # ================================================================== # # 모듈 수준 유틸리티 # ================================================================== # def _divider() -> QFrame: f = QFrame() f.setFrameShape(QFrame.HLine) f.setFixedHeight(1) f.setStyleSheet("background:#333333; border:none;") return f def _info_value(text: str) -> QLabel: lbl = QLabel(text) lbl.setStyleSheet("color:#ffffff; font-size:16px; font-weight:bold;") return lbl def _info_row(label: str, value_widget: QLabel) -> QHBoxLayout: row = QHBoxLayout() key = QLabel(label + ":") key.setFixedWidth(80) key.setStyleSheet("color:#888888; font-size:14px;") row.addWidget(key) row.addWidget(value_widget, stretch=1) return row