import sys from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QVBoxLayout, QRadioButton, QProgressBar, QFileDialog, QCheckBox, QHBoxLayout, QSpinBox, QLabel, QSizePolicy, QMessageBox, QTextEdit from PyQt5.QtCore import Qt, QThread, pyqtSlot, pyqtSignal import pandas as pd from web_action import MarketAutomation from logger_module import setup_logger import logging import asyncio class BrowserThread(QThread): progress_updated = pyqtSignal(int) # 진행 상태 업데이트를 위한 시그널 paused = pyqtSignal(bool) def __init__(self, url, market, deleting_data, logger, dryrunFactor, successCount): super().__init__() self.logger = logger self.dryrunFactor = dryrunFactor self.url = url self.market = market self.deleting_data = deleting_data self._pause = False self.successCount = successCount def run(self): self.logger.info(f"Browser thread starting for market: {self.market} with URL: {self.url}") asyncio.run(self.perform_deletion()) async def perform_deletion(self): automation = MarketAutomation() await automation.start_browser() self.logger.info("Browser started successfully.") await automation.goto_market(self.url) self.logger.info(f"Navigated to {self.url}") for index, product in enumerate(self.deleting_data): while self._pause: await asyncio.sleep(1) # 중지 상태에서 대기 result = await automation.search_and_delete_product(self.market, product, self.dryrunFactor) if result: self.logger.debug(f"상품삭제 성공 {index+1}/{len(self.deleting_data)} : {product} from {self.market}") self.progress_updated.emit(index + 1) # 진행 상태 업데이트를 위해 시그널 발생 self.successCount += 1 else: self.logger.debug(f"상품삭제 실패 : {product} from {self.market}") await automation.close_browser() self.logger.info("Browser closed successfully.") # QMessageBox.information(self, "완료", f"상품 [{self.successCount}]/[{len(self.deleting_data)}]개를 성공적으로 삭제했습니다.") def pause(self): self._pause = True def resume(self): self._pause = False class MainApp(QWidget): def __init__(self, logger): super().__init__() self.logger = logger self.deleting_data = [] self.browser_thread = None self.paused = False # 중지 상태를 추적하는 플래그 self.dryrunFactor = False self.successCount = 0 self.initUI() def initUI(self): mainLayout = QVBoxLayout() # 엑셀 파일 불러오기 및 관련 레이블 설정 topLayout = QHBoxLayout() # 엑셀 파일 불러오기 버튼 self.loadButton = QPushButton('엑셀 파일 불러오기', self) self.loadButton.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) self.loadButton.clicked.connect(self.load_excel) topLayout.addWidget(self.loadButton, 30) # 30% 공간 할당 # # 총 갯수 레이블 # self.totalCountLabel = QLabel("총 갯수: 0", self) # self.totalCountLabel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) # topLayout.addWidget(self.totalCountLabel, 20) # 20% 공간 할당 # 엑셀에서 불러온 셀값들의 갯수 레이블 self.loadedCountLabel = QLabel("불러온 갯수: 0", self) self.loadedCountLabel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) topLayout.addWidget(self.loadedCountLabel, 20) # 20% 공간 할당 # 글자수 레이블 self.byteSizeLabel = QLabel("글자수(Byte)", self) self.byteSizeLabel.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) topLayout.addWidget(self.byteSizeLabel, 10) # 10% 공간 할당 (20% 중 레이블 부분) # 글자수 스핀박스 self.byteSizeSpinBox = QSpinBox(self) self.byteSizeSpinBox.setRange(10, 200) self.byteSizeSpinBox.setValue(50) self.byteSizeSpinBox.setSingleStep(1) self.byteSizeSpinBox.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) topLayout.addWidget(self.byteSizeSpinBox, 10) # 10% 공간 할당 (20% 중 스핀박스 부분) mainLayout.addLayout(topLayout) # 라디오 버튼 가로 배열 radioLayout = QHBoxLayout() self.marketButtons = {name: QRadioButton(name) for name in ["쿠팡", "스마트스토어", "11번가", "롯데온", "ESM"]} for btn in self.marketButtons.values(): radioLayout.addWidget(btn) mainLayout.addLayout(radioLayout) # 버튼 레이아웃 (가로) buttonLayout = QHBoxLayout() # 브라우저 실행 버튼 self.startBrowserButton = QPushButton('브라우저 실행', self) self.startBrowserButton.clicked.connect(self.start_browser_action) buttonLayout.addWidget(self.startBrowserButton) # Dryrun 체크박스 self.dryRunCheckbox = QCheckBox('DryRUN', self) self.dryRunCheckbox.stateChanged.connect(self.dryrun) buttonLayout.addWidget(self.dryRunCheckbox) # 삭제 실행 버튼 self.deleteButton = QPushButton('삭제 실행', self) self.deleteButton.clicked.connect(self.delete_products) buttonLayout.addWidget(self.deleteButton) # 중지 버튼 self.pauseButton = QPushButton('중지', self) self.pauseButton.clicked.connect(self.toggle_pause_resume) buttonLayout.addWidget(self.pauseButton) # 버튼 레이아웃을 메인 레이아웃에 추가 mainLayout.addLayout(buttonLayout) # 진행 상태 프로그레스바 self.progressBar = QProgressBar(self) mainLayout.addWidget(self.progressBar) # 데이터 표시를 위한 QTextEdit 위젯 self.dataDisplay = QTextEdit(self) self.dataDisplay.setReadOnly(True) # 읽기 전용 설정 self.dataDisplay.setPlaceholderText("불러온 데이터가 여기에 표시됩니다...") mainLayout.addWidget(self.dataDisplay) # 항상위 체크박스 self.alwaysOnTopCheckbox = QCheckBox('항상위', self) self.alwaysOnTopCheckbox.stateChanged.connect(self.set_always_on_top) mainLayout.addWidget(self.alwaysOnTopCheckbox) # # Dryrun 체크박스 # self.dryRunCheckbox = QCheckBox('DryRUN', self) # self.dryRunCheckbox.stateChanged.connect(self.dryrun) # mainLayout.addWidget(self.dryRunCheckbox) self.setLayout(mainLayout) self.setGeometry(300, 300, 400, 600) self.setWindowTitle('상품삭제 Automation') def update_progress_bar(self, value): self.progressBar.setValue(value) # 프로그레스바 업데이트 def toggle_pause_resume(self): if self.browser_thread and self.browser_thread.isRunning(): if not self.paused: self.browser_thread.pause() self.pauseButton.setText('계속') self.paused = True else: self.browser_thread.resume() self.pauseButton.setText('중지') self.paused = False def dryrun(self, checked): if checked: self.dryrunFactor = True else: self.dryrunFactor = False @pyqtSlot() def load_excel_ini(self): fileName, _ = QFileDialog.getOpenFileName(self, 'Open Excel File', '', 'Excel Files (*.xlsx *.xls)') if fileName: byte_size = self.byteSizeSpinBox.value() try: df = pd.read_excel(fileName, sheet_name=0) # 첫 번째 시트를 불러옵니다. first_column_name = df.columns[0] # 첫 번째 열의 이름을 가져옵니다. # 결측치 제거 non_empty_data = df[first_column_name].dropna() # 중복 데이터 제거 unique_data = non_empty_data.drop_duplicates() # 바이트 크기 이상의 데이터 필터링 filtered_data = [data for data in unique_data if len(str(data).encode('utf-8')) >= byte_size] # 프로그레스바와 레이블 업데이트 self.progressBar.setMaximum(len(unique_data)) # 중복 제거된 전체 데이터 수를 프로그레스바 최대값으로 설정 self.loadedCountLabel.setText(f"불러온 갯수: {len(filtered_data)}") # 필터링된 데이터 수 # self.totalCountLabel.setText(f"총 갯수: {len(unique_data)}") # 중복 제거된 총 데이터 수 업데이트 self.deleting_data = filtered_data self.logger.debug(f'{fileName} 파일에서 중복 제거 후 {len(filtered_data)}개의 적합한 데이터를 성공적으로 불러왔습니다.') except Exception as e: self.logger.error(f"파일을 불러오는 중 오류가 발생했습니다: {e}") QMessageBox.critical(self, "오류", "파일을 불러오는 중 오류가 발생했습니다. 파일 형식을 확인해주세요.") @pyqtSlot() def load_excel(self): fileName, _ = QFileDialog.getOpenFileName(self, 'Open Excel File', '', 'Excel Files (*.xlsx *.xls)') if fileName: byte_size = self.byteSizeSpinBox.value() try: df = pd.read_excel(fileName, sheet_name=0) first_column_name = df.columns[0] non_empty_data = df[first_column_name].dropna() unique_data = non_empty_data.drop_duplicates() filtered_data = [data for data in unique_data if len(str(data).encode('utf-8')) >= byte_size] # 데이터를 QTextEdit 위젯에 표시 self.dataDisplay.clear() self.dataDisplay.append("\n".join([str(data) for data in filtered_data])) # 프로그레스바와 레이블 업데이트 self.progressBar.setMaximum(len(unique_data)) self.loadedCountLabel.setText(f"불러온 갯수: {len(filtered_data)}") self.deleting_data = filtered_data self.logger.debug(f'{fileName} 파일에서 중복 제거 후 {len(filtered_data)}개의 적합한 데이터를 성공적으로 불러왔습니다.') except Exception as e: self.logger.error(f"파일을 불러오는 중 오류가 발생했습니다: {e}") QMessageBox.critical(self, "오류", "파일을 불러오는 중 오류가 발생했습니다. 파일 형식을 확인해주세요.") @pyqtSlot() def start_browser_action(self): selected_market = next((btn.text() for btn in self.marketButtons.values() if btn.isChecked()), None) if not selected_market: # 마켓이 선택되지 않았을 경우 QMessageBox.warning(self, "경고", "마켓을 선택해주세요.") return # 경고창을 표시하고 함수를 더 이상 진행하지 않음 if not self.deleting_data: # 마켓이 선택되지 않았을 경우 QMessageBox.warning(self, "경고", "삭제대상이 없습니다.") return # 경고창을 표시하고 함수를 더 이상 진행하지 않음 if selected_market: urls = { "쿠팡": "https://xauth.coupang.com/auth/realms/seller/protocol/openid-connect/auth?response_type=code&client_id=wing&redirect_uri=https%3A%2F%2Fwing.coupang.com%2Fsso%2Flogin?returnUrl%3D%252F&state=794abcff-4d9d-4460-86d8-e0efba1b97c2&login=true&scope=openid", "스마트스토어": "https://accounts.commerce.naver.com/login?url=https%3A%2F%2Fsell.smartstore.naver.com%2F%23%2Flogin-callback", "11번가": "https://login.11st.co.kr/auth/front/selleroffice/login.tmall?returnURL=https%3A%2F%2Fsoffice.11st.co.kr%2F", "롯데온": "https://store.lotteon.com/cm/main/login_SO.wsp", "ESM": "https://signin.esmplus.com/login" } url = urls.get(selected_market) if url: if self.browser_thread and self.browser_thread.isRunning(): QMessageBox.warning(self, "경고", "이미 실행 중인 작업이 있습니다.") else: self.browser_thread = BrowserThread(url, selected_market, self.deleting_data, self.logger, self.dryrunFactor, self.successCount) self.browser_thread.progress_updated.connect(self.update_progress_bar) self.browser_thread.start() else: QMessageBox.critical(self, "오류", "선택한 마켓의 URL 정보가 없습니다.") @pyqtSlot() def delete_products(self): if self.browser_thread: self.browser_thread.start() def set_always_on_top(self, checked): if checked: self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) else: self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) self.show() if __name__ == '__main__': app = QApplication(sys.argv) # 로그 레벨을 DEBUG로 설정하여 로거를 초기화합니다. logger = setup_logger('default_logger', 'application.log', level=logging.DEBUG) ex = MainApp(logger) ex.show() sys.exit(app.exec_())