웨일 안전장치

This commit is contained in:
9700X_PC 2024-11-16 10:26:28 +09:00
parent 1f89ffa3a7
commit 90af0f04e1
15 changed files with 301 additions and 172 deletions

14
gui.py
View File

@ -107,6 +107,9 @@ class AutoPercentyGUI(QWidget):
# 이전에 저장된 설정 불러오기
self.load_settings()
# 로거 초기화
self.add_text_edit_logger()
# 프로그래스바 초기화
self.update_total_progress(0,0)
@ -140,6 +143,17 @@ class AutoPercentyGUI(QWidget):
self.logger.error(f"DB 파일 복사 중 오류 발생: {e}", exc_info=True)
raise e
def add_text_edit_logger(self):
"""QTextEdit에 로그를 출력하기 위한 핸들러 추가"""
text_edit_logger = QTextEditLogger()
text_edit_logger.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
# text_edit_logger.appendHtml.connect(self.log.appendHtml)
text_edit_logger.appendHtml.connect(self.log.append) # appendHtml 대신 append로 수정
text_edit_logger.scrollToBottom.connect(lambda: self.log.verticalScrollBar().setValue(self.log.verticalScrollBar().maximum()))
self.logger.addHandler(text_edit_logger)
self.logger.debug('로그기록이 설정되었습니다.')
def start_stage(self, stage_index):
"""지정한 단계에 깜빡임 효과 적용"""
if 0 <= stage_index < len(self.stage_labels):

Binary file not shown.

View File

@ -1,8 +1,7 @@
import logging
import os
from logging.handlers import RotatingFileHandler
from PySide6.QtCore import Signal, QObject # PyQt5.QtCore.pyqtSignal -> PySide6.QtCore.Signal
from PySide6.QtWidgets import QTextEdit
from PyQt5.QtCore import pyqtSignal, QObject
def setup_logger(name, log_file, level=logging.DEBUG, max_bytes=20*1024*1024, backup_count=5):
"""로거 설정을 위한 함수"""
@ -24,84 +23,34 @@ def setup_logger(name, log_file, level=logging.DEBUG, max_bytes=20*1024*1024, ba
logger.addHandler(console_handler)
return logger
class QTextEditLogger(logging.Handler, QObject):
"""
PySide6 QTextEdit와 연동하여 로그 메시지를 출력하는 로깅 핸들러.
"""
appendHtml = Signal(str) # HTML 메시지를 전달하는 시그널
scrollToBottom = Signal() # 텍스트 영역의 스크롤을 최하단으로 이동시키는 시그널
appendHtml = pyqtSignal(str) # HTML 메시지를 전달할 시그널 정의
scrollToBottom = pyqtSignal() # 스크롤을 최하단으로 이동시키는 시그널
def __init__(self, text_edit):
"""
QTextEditLogger 초기화.
:param text_edit: 로그 메시지를 표시할 QTextEdit 위젯
"""
QObject.__init__(self) # QObject 초기화
logging.Handler.__init__(self) # logging.Handler 초기화
self.text_edit = text_edit # 로그를 출력할 QTextEdit 위젯
# 시그널 연결
self.appendHtml.connect(self.text_edit.append)
self.scrollToBottom.connect(self.scroll_to_bottom)
def __init__(self):
logging.Handler.__init__(self)
QObject.__init__(self)
def emit(self, record):
"""
로그 레코드를 처리하고 QTextEdit에 출력.
:param record: 로그 레코드 (LogRecord 객체)
"""
try:
if not hasattr(record, "getMessage"):
raise ValueError(f"Invalid LogRecord: {record}")
# 로그 레코드를 문자열로 포매팅
msg = self.format(record) # format(record)는 getMessage를 호출
color = {
logging.DEBUG: "black",
logging.INFO: "grey",
logging.WARNING: "orange",
logging.ERROR: "red",
logging.CRITICAL: "purple",
}.get(record.levelno, "black")
# HTML 스타일 적용한 메시지 생성
html_message = f"<span style=\"color:{color};\">{msg}</span><br/>"
# 디버깅 메시지 출력
print(f"Emitting signal with message: {html_message}")
# Signal.emit 호출 (문자열 하나만 전달)
try:
self.appendHtml.emit(html_message)
except TypeError as te:
print(f"TypeError while emitting appendHtml signal: {te}")
except Exception as e:
print(f"Error while emitting appendHtml signal: {e}")
# 스크롤 최하단 이동
self.scrollToBottom.emit()
except ValueError as ve:
print(f"ValueError in emit: {ve}")
except Exception as e:
print(f"Error in emit: {e}")
def scroll_to_bottom(self):
"""
QTextEdit의 스크롤을 최하단으로 이동.
"""
self.text_edit.verticalScrollBar().setValue(
self.text_edit.verticalScrollBar().maximum()
)
msg = self.format(record) # 로그 레코드를 문자열로 포매팅
color = {
logging.DEBUG: "black",
logging.INFO: "grey",
logging.WARNING: "orange",
logging.ERROR: "red",
logging.CRITICAL: "purple",
}.get(record.levelno, "black")
# HTML 스타일을 적용한 메시지 생성
message = f"<span style=\"color:{color};\">{msg}</span><br/>"
self.appendHtml.emit(message) # HTML 메시지로 변경
self.scrollToBottom.emit() # 스크롤 시그널 발생
def close(self):
"""
핸들러를 닫고 필요한 정리 작업 수행.
"""
self.flush()
super().close()
logging.Handler.close(self)
def flush(self):
"""
필요 출력 버퍼를 비움.
"""
pass
pass # 필요 시 정리 작업 수행

107
logger_module_pyside6.py Normal file
View File

@ -0,0 +1,107 @@
import logging
import os
from logging.handlers import RotatingFileHandler
from PySide6.QtCore import Signal, QObject # PyQt5.QtCore.pyqtSignal -> PySide6.QtCore.Signal
from PySide6.QtWidgets import QTextEdit
def setup_logger(name, log_file, level=logging.DEBUG, max_bytes=20*1024*1024, backup_count=5):
"""로거 설정을 위한 함수"""
formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s')
# RotatingFileHandler를 사용하여 로그 파일 설정
handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8')
handler.setFormatter(formatter)
logger = logging.getLogger(name)
logger.setLevel(level)
logger.addHandler(handler)
# 콘솔 로그 출력을 위한 핸들러가 이미 추가되었는지 확인
if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
console_handler.setLevel(level)
logger.addHandler(console_handler)
return logger
class QTextEditLogger(logging.Handler, QObject):
"""
PySide6 QTextEdit와 연동하여 로그 메시지를 출력하는 로깅 핸들러.
"""
appendHtml = Signal(str) # HTML 메시지를 전달하는 시그널
scrollToBottom = Signal() # 텍스트 영역의 스크롤을 최하단으로 이동시키는 시그널
def __init__(self, text_edit):
"""
QTextEditLogger 초기화.
:param text_edit: 로그 메시지를 표시할 QTextEdit 위젯
"""
QObject.__init__(self) # QObject 초기화
logging.Handler.__init__(self) # logging.Handler 초기화
self.text_edit = text_edit # 로그를 출력할 QTextEdit 위젯
# 시그널 연결
self.appendHtml.connect(self.text_edit.append)
self.scrollToBottom.connect(self.scroll_to_bottom)
def emit(self, record):
"""
로그 레코드를 처리하고 QTextEdit에 출력.
:param record: 로그 레코드 (LogRecord 객체)
"""
try:
if not hasattr(record, "getMessage"):
raise ValueError(f"Invalid LogRecord: {record}")
# 로그 레코드를 문자열로 포매팅
msg = self.format(record) # format(record)는 getMessage를 호출
color = {
logging.DEBUG: "black",
logging.INFO: "grey",
logging.WARNING: "orange",
logging.ERROR: "red",
logging.CRITICAL: "purple",
}.get(record.levelno, "black")
# HTML 스타일 적용한 메시지 생성
html_message = f"<span style=\"color:{color};\">{msg}</span><br/>"
# 디버깅 메시지 출력
print(f"Emitting signal with message: {html_message}")
# Signal.emit 호출 (문자열 하나만 전달)
try:
self.appendHtml.emit(html_message)
except TypeError as te:
print(f"TypeError while emitting appendHtml signal: {te}")
except Exception as e:
print(f"Error while emitting appendHtml signal: {e}")
# 스크롤 최하단 이동
self.scrollToBottom.emit()
except ValueError as ve:
print(f"ValueError in emit: {ve}")
except Exception as e:
print(f"Error in emit: {e}")
def scroll_to_bottom(self):
"""
QTextEdit의 스크롤을 최하단으로 이동.
"""
self.text_edit.verticalScrollBar().setValue(
self.text_edit.verticalScrollBar().maximum()
)
def close(self):
"""
핸들러를 닫고 필요한 정리 작업 수행.
"""
self.flush()
super().close()
def flush(self):
"""
필요 출력 버퍼를 비움.
"""
pass

View File

@ -1,56 +0,0 @@
# import logging
# import os
# from logging.handlers import RotatingFileHandler
# # from PyQt5.QtCore import pyqtSignal, QObject
# def setup_logger(name, log_file, level=logging.DEBUG, max_bytes=20*1024*1024, backup_count=5):
# """로거 설정을 위한 함수"""
# formatter = logging.Formatter('%(asctime)s - %(filename)s:%(lineno)d - %(name)s - %(levelname)s - %(message)s')
# # RotatingFileHandler를 사용하여 로그 파일 설정
# handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count, encoding='utf-8')
# handler.setFormatter(formatter)
# logger = logging.getLogger(name)
# logger.setLevel(level)
# logger.addHandler(handler)
# # 콘솔 로그 출력을 위한 핸들러가 이미 추가되었는지 확인
# if not any(isinstance(h, logging.StreamHandler) for h in logger.handlers):
# console_handler = logging.StreamHandler()
# console_handler.setFormatter(formatter)
# console_handler.setLevel(level)
# logger.addHandler(console_handler)
# return logger
# class QTextEditLogger(logging.Handler, QObject):
# appendHtml = pyqtSignal(str) # HTML 메시지를 전달할 시그널 정의
# scrollToBottom = pyqtSignal() # 스크롤을 최하단으로 이동시키는 시그널
# def __init__(self):
# logging.Handler.__init__(self)
# QObject.__init__(self)
# def emit(self, record):
# msg = self.format(record) # 로그 레코드를 문자열로 포매팅
# color = {
# logging.DEBUG: "black",
# logging.INFO: "grey",
# logging.WARNING: "orange",
# logging.ERROR: "red",
# logging.CRITICAL: "purple",
# }.get(record.levelno, "black")
# # HTML 스타일을 적용한 메시지 생성
# message = f"<span style=\"color:{color};\">{msg}</span><br/>"
# self.appendHtml.emit(message) # HTML 메시지로 변경
# self.scrollToBottom.emit() # 스크롤 시그널 발생
# def close(self):
# self.flush()
# logging.Handler.close(self)
# def flush(self):
# pass # 필요 시 정리 작업 수행

BIN
re.txt Normal file

Binary file not shown.

View File

@ -6,21 +6,35 @@ from cx_Freeze import setup, Executable
base_dir = os.path.dirname(__file__)
browsers_dir = os.path.join(base_dir, 'browsers')
chromium_dir = os.path.join(browsers_dir, 'chromium-1112')
whale_dir = os.path.join(browsers_dir, 'whale')
extensions_dir = os.path.join(browsers_dir, 'extensions')
user_data_dir = os.path.join(browsers_dir, 'user_data')
# 필요한 파일 정의
include_files = [
'config.ini',
'leensoo1nt.json',
'prompt.json',
'userDB.db',
('src/initialDB.db', 'src/initialDB.db'),
('src/Percenty_SS_Code.json', 'src/Percenty_SS_Code.json'),
(chromium_dir, 'browsers/chromium-1112'), # Chromium 브라우저 실행 파일
(extensions_dir, 'browsers/extensions'), # 확장 프로그램 폴더
(user_data_dir, 'browsers/user_data'), # 사용자 데이터 폴더
# # 필요한 파일 정의
# include_files = [
# 'config.ini',
# 'leensoo1nt.json',
# 'prompt.json',
# 'userDB.db',
# ('src/initialDB.db', 'src/initialDB.db'),
# ('src/Percenty_SS_Code.json', 'src/Percenty_SS_Code.json'),
# (chromium_dir, 'browsers/chromium-1112'), # Chromium 브라우저 실행 파일
# (extensions_dir, 'browsers/extensions'), # 확장 프로그램 폴더
# (user_data_dir, 'browsers/user_data'), # 사용자 데이터 폴더
# ]
include_files = [
os.path.abspath('config.ini'),
os.path.abspath('leensoo1nt.json'),
os.path.abspath('prompt.json'),
os.path.abspath('userDB.db'),
(os.path.abspath('src/initialDB.db'), 'src/initialDB.db'),
(os.path.abspath('src/Percenty_SS_Code.json'), 'src/Percenty_SS_Code.json'),
(os.path.abspath(chromium_dir), 'browsers/chromium-1112'),
(os.path.abspath(whale_dir), 'browsers/whale'),
(os.path.abspath(extensions_dir), 'browsers/extensions'),
(os.path.abspath(user_data_dir), 'browsers/user_data'),
]
# 사용된 패키지 정의
@ -42,7 +56,9 @@ build_exe_options = {
'excludes': [
'tkinter', 'PyQt4', 'PyQt6', 'AppKit', 'Foundation', 'IPython', 'OpenSSL', 'asyncpg',
'curses', 'pydantic', 'test', 'unittest', 'matplotlib', 'tensorflow', 'torch', 'scipy'
]
],
'silent': False # 디버그 메시지 활성화
}
# 애플리케이션 메인 파일 및 설정
@ -51,13 +67,13 @@ if sys.platform == 'win32':
base = 'Win32GUI'
executables = [
Executable('main.py', base=base, target_name='AutoPercenty2.exe')
Executable('main.py', base=base, target_name='AutoPercenty3.exe')
]
# Setup 설정
setup(
name='AutoPercenty2',
version='1.1',
name='AutoPercenty3',
version='3.1',
description='자동화도구',
options={'build_exe': build_exe_options},
executables=executables

65
setup1.py Normal file
View File

@ -0,0 +1,65 @@
import os
from cx_Freeze import setup, Executable
# 필요한 파일 경로 설정
base_dir = os.path.dirname(__file__)
browsers_dir = os.path.join(base_dir, 'browsers')
chromium_dir = os.path.join(browsers_dir, 'chromium-1112')
whale_dir = os.path.join(browsers_dir, 'whale')
extensions_dir = os.path.join(browsers_dir, 'extensions')
user_data_dir = os.path.join(browsers_dir, 'user_data')
# 포함할 파일 설정
include_files = [
os.path.abspath('config.ini'),
os.path.abspath('leensoo1nt.json'),
os.path.abspath('prompt.json'),
os.path.abspath('userDB.db'),
(os.path.abspath('src/initialDB.db'), 'src/initialDB.db'),
(os.path.abspath('src/Percenty_SS_Code.json'), 'src/Percenty_SS_Code.json'),
(os.path.abspath('browsers/chromium-1112'), 'browsers/chromium-1112'),
(os.path.abspath('browsers/whale'), 'browsers/whale'),
(os.path.abspath('browsers/extensions'), 'browsers/extensions'),
(os.path.abspath('browsers/user_data'), 'browsers/user_data'),
]
# 빌드 옵션 정의
build_exe_options = {
'packages': [
'ctypes', 'asyncio', 'os', 're', 'time', 'math', 'json', 'logging', 'shutil', 'random',
'base64', 'subprocess', 'configparser', 'pyperclip', 'numpy', 'cv2', 'requests',
'win32clipboard', 'win32gui', 'win32con', 'win32process', 'PIL', 'bs4', 'pyautogui',
'pyvda', 'sqlalchemy', 'sqlalchemy.orm', 'sqlalchemy.exc', 'collections', 'pandas'
],
'includes': [
'PySide6.QtWidgets', 'PySide6.QtCore', 'PySide6.QtGui', 'PyQt5.QtCore',
'whale_translator', 'gui', 'logger_module', 'toggleSwitch',
'browser_control', 'clipboardImageManager', 'vertexAI', 'option',
'price', 'title', 'locatorManager', 'src.cmb_diag', 'src.DatabaseManager',
'vertexai.generative_models'
],
'include_files': include_files,
'excludes': [
'tkinter', 'PyQt4', 'PyQt6', 'AppKit', 'Foundation', 'IPython', 'OpenSSL', 'asyncpg',
'curses', 'pydantic', 'test', 'unittest', 'matplotlib', 'tensorflow', 'torch', 'scipy'
],
'silent': False # 디버그 메시지 활성화
}
# 실행 파일 정의
executables = [
Executable(
script='main.py', # 메인 파일
target_name='MyApp.exe', # 생성될 실행 파일 이름
base='Win32GUI' # GUI 애플리케이션의 경우 사용
)
]
# setup 함수
setup(
name="MyApplication",
version="1.0",
description="My Python application",
options={'build_exe': build_exe_options},
executables=executables
)

View File

@ -516,9 +516,11 @@ class CMBSettingsDialog(QDialog):
# 쿼리 실행 및 데이터 가져오기
rows = self.db_manager.fetchall(query, params)
self.logger.debug(f"쿼리 실행 및 데이터 가져오기")
# id 기준 오름차순 정렬을 위해 정렬
rows = sorted(rows, key=lambda x: int(x[0])) # x[0]는 id 열
self.logger.debug(f"sorted - 오름차순/내림차순으로 정렬")
for row_data in rows:
row_id, category1, category2, category3, category4, crmobi_stage = row_data

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 KiB

After

Width:  |  Height:  |  Size: 696 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 624 KiB

After

Width:  |  Height:  |  Size: 789 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 856 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 401 KiB

After

Width:  |  Height:  |  Size: 535 KiB

BIN
userDB.db

Binary file not shown.

View File

@ -1,10 +1,12 @@
import time
import os, sys
import re
import pyperclip
from pywinauto import Application, findwindows, clipboard, timings
from pywinauto.controls.hwndwrapper import HwndWrapper
from PIL import ImageGrab
from pywinauto.findwindows import ElementNotFoundError
from pywinauto.timings import wait_until
import re
class WhaleTranslator:
def __init__(self, logger):
@ -120,33 +122,61 @@ class WhaleTranslator:
def navigate_to_url(self, url):
"""주소창에 URL을 입력하고 페이지 로딩을 대기"""
try:
# URL을 클립보드에 복사
pyperclip.copy(url)
retry_count = 5 # 주소창 찾기 재시도 횟수
for attempt in range(retry_count):
try:
# URL을 클립보드에 복사
pyperclip.copy(url)
# 주소창을 클릭하여 URL 붙여넣기
address_bar = self.whale_window.child_window(title="주소창 및 검색창", control_type="Edit")
address_bar.click_input()
# 주소창을 클릭하여 URL 붙여넣기
address_bar = self.whale_window.child_window(title="주소창 및 검색창", control_type="Edit")
address_bar.click_input()
# Ctrl + V로 URL 붙여넣기 후 Enter 키 입력
address_bar.type_keys("^v{ENTER}", with_spaces=True)
self.logger.debug(f"{url}로 이동 중...")
# Ctrl + V로 URL 붙여넣기 후 Enter 키 입력
address_bar.type_keys("^v{ENTER}", with_spaces=True)
self.logger.debug(f"{url}로 이동 중...")
# 5초 동안 0.1초 간격으로 이미지 요소가 나타나는지 검사
start_time = time.time()
while time.time() - start_time < 5:
try:
# 특정 이미지 요소를 찾으면 즉시 반환
image_element = self.whale_window.child_window(title="누락된 이미지 설명을 확인하려면 컨텍스트 메뉴를 여세요.", control_type="Image")
if image_element.exists(timeout=0.5):
self.logger.debug("페이지 로딩 완료: 이미지 요소가 나타났습니다.")
return
except Exception:
pass # 요소가 아직 나타나지 않은 경우 대기
# 10초 동안 0.5초 간격으로 이미지 요소가 나타나는지 검사
start_time = time.time()
while time.time() - start_time < 10:
try:
# 이미지 요소를 찾으면 즉시 반환
image_element = self.whale_window.child_window(
title="누락된 이미지 설명을 확인하려면 컨텍스트 메뉴를 여세요.",
control_type="Image"
)
if image_element.exists(timeout=1):
self.logger.debug("페이지 로딩 완료: 이미지 요소가 나타났습니다.")
return
except Exception as e:
self.logger.debug(f"이미지 요소 확인 중 오류 발생: {e}")
self.logger.error("지정된 시간 내에 이미지 요소를 찾지 못했습니다.")
except Exception as e:
self.logger.error(f"주소창에 접근할 수 없습니다: {e}", exc_info=True)
# 안전장치 1: 현재 창 제목 확인
window_title = self.whale_window.window_text()
if re.match(r".+\.(jpg|png) \(\d+x\d+\)", window_title, re.IGNORECASE):
self.logger.info(f"창 제목에 이미지 파일과 해상도가 감지됨: {window_title}")
return
# 안전장치 2: 현재 창의 컨트롤 핸들 요소 출력
self.logger.error("지정된 시간 내에 이미지 요소를 찾지 못했습니다. 현재 창의 컨트롤 요소:")
with open("debug_controls.txt", "w", encoding="utf-8") as f:
original_stdout = os.sys.stdout
os.sys.stdout = f
self.whale_window.print_control_identifiers()
os.sys.stdout = original_stdout
self.logger.info("컨트롤 식별자가 debug_controls.txt에 저장되었습니다.")
except ElementNotFoundError as e:
self.logger.error("주소창 요소를 찾을 수 없습니다. 창을 다시 검색합니다.", exc_info=True)
self.whale_window = self.find_whale_window() # 창을 다시 찾음
if not self.whale_window:
self.logger.error("웨일 창을 다시 찾을 수 없습니다. 작업 중단.")
return
except Exception as e:
self.logger.error(f"주소창 접근 중 오류 발생: {e}", exc_info=True)
self.logger.error("주소창 찾기 및 URL 이동 시도 횟수를 초과했습니다.")
def navigate_to_url_for_typing(self, url):
"""주소창에 URL을 입력하고 페이지 로딩을 대기"""
@ -251,6 +281,8 @@ class WhaleTranslator:
"""창 제목에서 이미지의 너비와 높이를 추출하여 반환합니다."""
try:
window_title = self.whale_window.window_text()
self.logger.debug(f"Window Title: {window_title}")
# 창 제목에서 해상도 추출 시도
match = re.search(r"\((\d+)×(\d+)\)", window_title)
if match:
@ -266,7 +298,7 @@ class WhaleTranslator:
return True # 해상도 조건을 만족하면 작업을 진행
# 해상도가 없다면, 파일 확장자를 확인하여 번역 여부 결정
elif window_title.endswith(".jpg") or window_title.endswith(".png"):
elif ".jpg" in window_title or ".png" in window_title:
self.logger.info("이미지 해상도가 없지만, 파일 확장자가 .jpg 또는 .png입니다. 번역 작업을 진행합니다.")
return True # 해상도가 없어도 번역 작업을 수행