Files
ant-vision-inspector/db/sql_client.py
2026-06-18 13:38:27 +09:00

269 lines
9.2 KiB
Python

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