import pyodbc # 선호 순서 — 앞쪽일수록 우선. 설치 환경마다 다를 수 있어 자동 탐지 후 선택. _PREFERRED_DRIVERS = ( "ODBC Driver 18 for SQL Server", "ODBC Driver 17 for SQL Server", "ODBC Driver 13 for SQL Server", "SQL Server Native Client 11.0", "SQL Server", ) def _pick_driver() -> "str | None": """이 PC에 설치된 ODBC 드라이버 중 사용할 SQL Server 드라이버를 선택. 선호 순서대로 먼저 찾고, 없으면 이름에 'SQL Server'가 포함된 아무 드라이버라도 사용.""" available = pyodbc.drivers() for name in _PREFERRED_DRIVERS: if name in available: return name for name in available: if "SQL Server" in name: return name return None class SQLClient: def __init__(self): self.conn = None self.cursor = None def connect(self, server: str, database: str, username: str, password: str) -> bool: driver = _pick_driver() if driver is None: print("[DB] 연결 실패: 설치된 SQL Server ODBC 드라이버가 없습니다. " "'ODBC Driver 18 for SQL Server'를 설치하세요.") self.conn = None return False try: conn_str = ( f"DRIVER={{{driver}}};" f"SERVER={server};" f"DATABASE={database};" f"UID={username};" f"PWD={password};" f"TrustServerCertificate=yes;" f"Encrypt=optional;" ) self.conn = pyodbc.connect(conn_str, timeout=10) self.cursor = self.conn.cursor() print(f"[DB] 연결 성공: {server}/{database} (드라이버: {driver})") return True except Exception as e: print(f"[DB] 연결 실패: {e} (드라이버: {driver})") self.conn = None return False def disconnect(self): if self.cursor: self.cursor.close() if self.conn: self.conn.close() self.conn = None self.cursor = None print("[DB] 연결 해제") def is_connected(self) -> bool: return self.conn is not None @staticmethod def _norm_id(value) -> str: return str(value).strip() if value is not None else "" def get_wk_results(self) -> list: """ vi_AI_WK_Result 뷰 조회. 반환: [{ "article_id": ..., "machine_id": ..., "machine": ..., "work_start_date": ..., "work_start_time": ..., }, ...] """ if not self.is_connected(): return [] try: self.cursor.execute(""" SELECT ArticleID, MachineID, Machine, WorkStartDate, WorkStartTime FROM vi_AI_WK_Result WHERE ArticleID IS NOT NULL ORDER BY ArticleID """) rows = self.cursor.fetchall() return [self._row_to_wk_result(row) for row in rows] except Exception as e: print(f"[DB] WK_Result 조회 실패: {e}") return [] def get_wk_result_article_ids(self) -> list: """vi_AI_WK_Result에 있는 ArticleID 목록 (중복 제거, 순서 유지).""" seen = set() ids = [] for row in self.get_wk_results(): norm = self._norm_id(row["article_id"]) if norm and norm not in seen: seen.add(norm) ids.append(row["article_id"]) return ids def get_wk_result_map(self) -> dict: """ArticleID(정규화) → WK_Result 행. 동일 ID가 여러 행이면 마지막 행 사용.""" result = {} for row in self.get_wk_results(): norm = self._norm_id(row["article_id"]) if norm: result[norm] = row return result @staticmethod def _row_to_wk_result(row) -> dict: return { "article_id": row[0], "machine_id": row[1], "machine": row[2], "work_start_date": row[3], "work_start_time": row[4], } @staticmethod def format_db_value(value) -> str: if value is None: return "—" if hasattr(value, "strftime"): if hasattr(value, "hour"): return value.strftime("%H:%M:%S") return value.strftime("%Y-%m-%d") text = str(value).strip() return text if text else "—" def get_reflector_list_ordered(self, article_ids: list) -> list: """article_ids 순서를 유지한 제품 목록 (PatMax 슬롯 순서용).""" if not article_ids: return [] by_norm = { self._norm_id(item["article_id"]): item for item in self.get_reflector_list(article_ids=article_ids) } ordered = [] for article_id in article_ids: item = by_norm.get(self._norm_id(article_id)) if item is not None: ordered.append(item) return ordered def split_articles_by_wk(self, mes_selected_ids: "list | None" = None) -> tuple: """ vi_AI_mt_Article 목록을 WK_Result 작업 대상 / 기타로 분류. 반환: (active_list, inactive_list) """ if mes_selected_ids is not None: if len(mes_selected_ids) == 0: return [], [] all_items = self.get_reflector_list_ordered(mes_selected_ids) else: all_items = self.get_reflector_list() if not all_items: return [], [] wk_norm = set(self.get_wk_result_map().keys()) active = [ item for item in all_items if self._norm_id(item["article_id"]) in wk_norm ] inactive = [ item for item in all_items if self._norm_id(item["article_id"]) not in wk_norm ] return active, inactive def get_inspectable_articles(self, mes_selected_ids: "list | None" = None) -> list: """ vi_AI_mt_Article ∩ vi_AI_WK_Result. mes_selected_ids 지정 시 관리자 MES 선택과도 교집합. """ wk_ids = self.get_wk_result_article_ids() if not wk_ids: return [] wk_by_norm = {self._norm_id(article_id): article_id for article_id in wk_ids} if mes_selected_ids is not None: if len(mes_selected_ids) == 0: return [] filter_ids = [ wk_by_norm[self._norm_id(article_id)] for article_id in mes_selected_ids if self._norm_id(article_id) in wk_by_norm ] else: filter_ids = wk_ids if not filter_ids: return [] return self.get_reflector_list(article_ids=filter_ids) def get_all_articles(self) -> list: """ vi_AI_mt_Article 뷰 전체 조회 (관리자 MES 제품 선택용). 반환: [{"article_id": ..., "article": ..., "buyer_article_no": ...}, ...] """ if not self.is_connected(): return [] try: self.cursor.execute(""" SELECT ArticleID, Article, BuyerArticleNo FROM vi_AI_mt_Article ORDER BY ArticleID """) rows = self.cursor.fetchall() return [self._row_to_article(row) for row in rows] except Exception as e: print(f"[DB] 전체 제품 조회 실패: {e}") return [] def get_reflector_list(self, article_ids: "list | None" = None) -> list: """ vi_AI_mt_Article 뷰에서 제품 목록 조회. article_ids 지정 시 해당 ID만, 미지정 시 REF 포함 제품 전체. 반환: [{"article_id": ..., "article": ..., "buyer_article_no": ...}, ...] """ if not self.is_connected(): return [] try: if article_ids: placeholders = ",".join("?" * len(article_ids)) self.cursor.execute(f""" SELECT ArticleID, Article, BuyerArticleNo FROM vi_AI_mt_Article WHERE ArticleID IN ({placeholders}) ORDER BY ArticleID """, article_ids) else: self.cursor.execute(""" SELECT ArticleID, Article, BuyerArticleNo FROM vi_AI_mt_Article WHERE Article LIKE '%REF%' ORDER BY ArticleID """) rows = self.cursor.fetchall() return [self._row_to_article(row) for row in rows] except Exception as e: print(f"[DB] 조회 실패: {e}") return [] @staticmethod def _row_to_article(row) -> dict: return { "article_id": row[0], "article": row[1], "buyer_article_no": row[2], } def save_inspection_result(self, article_id: str, result: str, score: float) -> bool: """검사 결과 저장 — 테이블 확정 후 구현.""" # TODO: 결과 저장 테이블 확정 후 쿼리 구현 print(f"[DB] 검사 결과 저장: {article_id} {result} {score}") return True