diff --git a/main.py b/main.py index ceed7c8..a9766c0 100644 --- a/main.py +++ b/main.py @@ -156,7 +156,7 @@ class MainApp(QWidget): self.paused = False @pyqtSlot() - def load_excel(self): + def load_excel_ini(self): fileName, _ = QFileDialog.getOpenFileName(self, 'Open Excel File', '', 'Excel Files (*.xlsx *.xls)') if fileName: byte_size = self.byteSizeSpinBox.value() @@ -179,9 +179,41 @@ class MainApp(QWidget): 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 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", @@ -191,15 +223,16 @@ class MainApp(QWidget): "ESM": "https://signin.esmplus.com/login" } url = urls.get(selected_market) - if not selected_market: - QMessageBox.warning(self, "경고", "마켓을 선택해 주세요.") + if url: + if self.browser_thread and self.browser_thread.isRunning(): + QMessageBox.warning(self, "경고", "이미 실행 중인 작업이 있습니다.") else: - 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.browser_thread.progress_updated.connect(self.update_progress_bar) - self.browser_thread.start() + self.browser_thread = BrowserThread(url, selected_market, self.deleting_data, self.logger) + self.browser_thread.progress_updated.connect(self.update_progress_bar) + self.browser_thread.start() + else: + QMessageBox.critical(self, "오류", "선택한 마켓의 URL 정보가 없습니다.") + @pyqtSlot() def delete_products(self): diff --git a/prompt.txt b/prompt.txt new file mode 100644 index 0000000..332b9dc --- /dev/null +++ b/prompt.txt @@ -0,0 +1,148 @@ +너는 파이썬 프로그래밍 전문가지? 나는 아래의 동작을 하는 간단한 프로그램을 만들고 싶어하는 사업가야. 내게 친절하고 자세하게 설명해 주면서 프로그램을 만드는데 도와줄꺼지?? +하나하나 차근차근 단계별로 만들어가보자. + +pyqt5로 gui인터페이스를 가진 간단한 프로그램을 만들어줘. +디버깅을 위해 아래에 첨부한 로깅모듈을 넣고, 레벨은 디버그로 설정, 각 동작마다 디버그메세지를 출력해줘. 콘솔과 파일 모두 출력되어야 해. +그리고 예외처리를 위해 모든 동작들은 강건하게 처리되어야 하고, 로그메세지에는 exc_info=true 설정이 되어있어야 해. +playwright동작 부분은 별도의 클래스와 클래스 메서드로 만들어져서 모듈화를 시키고 유지보수성을 향상시켜야 해. + +GUI구성 +화면구성은 간단해. +1. 엑셀파일 불러오기 버튼으로 엑셀파일을 불러와. +2. 라디오 박스가 5개 있어. 각 라벨은 "쿠팡, 스스, 11번가, 롯데온, ESM"이야. +3. 브라우저 실행 버튼이 있고, 이걸 누르면 비동기 playwright가 실행되. 이때 접속하는 url은 라디오 박스에서 선택한 사이트의 판매자센터로 접속해. +4. 삭제실행 버튼이 있고, 이걸 누르면 삭제메서드를 불러와 실행되. +5. 중지버튼이 있고, 이 버튼은 삭제실행의 메서드를 중지시켜. 이렇게 되면 중지 버튼의 텍스트는 "계속"으로 바껴. +6. 진행상태 프로그레스바 가 있어. +7. 항상위 버튼 체크박스가 있어. 이걸 체크하면 현재 실행중인 프로그램이 항상위로 설정되. +8. 인증요청 버튼이 있고, 해당 버튼을 누르면 서버에서 인증키를 발급하고 해당 키를 프로그램 설정에 자동으로 입력되. + + +# 로그 및 서버설정 +프로그램이 실행되면 mongoDB를 이용해 사용자 인증을 거치고, 인증된 PC는 로그를 기록해야 해. 인증 콜렉션은 +SideProject 데이터베이스에 WRMC_DeleteProduct_Log 콜렉션에 접속한 PC의 IP와 PC이름, 접속한 시간이 기록되어야 해. +(데이터베이스와 콜렉션이 존재하지 않으면 생성해야 해) +그리고 프로그램이 종료되면 (사용자가 종료하든, 예외발생이나 강제종료로 종료되든) 프로그램 로그를 해당 PC에 기록해 줘. + + +동작구성 +1. 엑셀파일 불러오기 버튼을 누르면 파일 다이알로그가 열리고, 엑셀파일을 선택해. +그리고 입력받은 엑셀파일의 첫 열에서 값들을 가져와 일정 바이트 이상의 길이인 셀값을 deleting_data 리스트 변수로 넣어줘. + +2. 보통 해당값은 10개~200개 사이의 값이 될꺼야. 이 전체 갯수는 진행상태 프로그레스 바에 사용될꺼야. + +3. 라디오박스에서 선택되는 마켓들마다 playwright에서 접속하는 주소가 달라져. +쿠팡 : 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 + +4. 브라우저 실행버튼을 누르면 playwright는 비동기로 실행되어서 라디오 버튼에서 선택된 사이트로 접속되. 이때 gui프로그램의 응답성을 위해 비동기로 실행되고 사용자와 상호작용하도록 해야해. 실행모드는 headless=False야. +5. 브라우저가 실행되어 해당 마켓에 접속되면 사용자는 아이디와 비밀번호를 입력하고 해당 마켓에 접속 후, 상품수정페이지로 접속할 꺼야. +6. 상품수정 페이지에 접속완료되면 사용자는 삭제실행 버튼을 누를꺼야. +7. 삭제 실행 메서드의 동작은 playwright에서 접속된 사이트의 상품수정 페이지에서 동작해. deleting_data 리스트의 값을 순차적으로 "검색어" xpath에 넣고, "검색" xpath 버튼을 눌러. 그리고 검색결과가 나오면 "체크박스" xpath를 클릭하고, "삭제" xpath 버튼을 누를꺼야. 그럼 "검색결과" xpath에는 아무 데이터가 없어. +8. 7의 동작은 리스트에 있는 모든 값을 순차적으로 실행하면되. 1건 삭제동작이 성공하면 진행상태 프로그레스 바를 업데이트 시켜줘. +9. 동작 중 사용자가 중지버튼을 누르면 7의 동작은 중지되어야 해. 그리고 중지버튼의 텍스트는 "계속"으로 바껴. 그리고 사용자가 다시 "계속"텍스트로 바뀐 중지버튼을 누르면 7의 동작이 멈춘 부분부터 다시 재개되. +10. deleting_data 리스트의 끝에 다다르면 프로그레스바는 100%가 되어있을 것이고, 사용자에게 작업완료 라는 텍스트 메세지를 띄워줘. + +11. 웹요소는 아래와 같아. +[ESM] +1.검색어 입력 박스 + - 웹 요소 : + - XPATH : /html/body/div[1]/div[3]/div[3]/div/div[1]/div[1]/dl[1]/dd/div/textarea + +2.검색버튼 + - 웹 요소 : + 검색하기 + + - XPATH : /html/body/div[1]/div[3]/div[3]/div/div[3]/div[2]/a[1] + +3. 체크박스 + - 웹 요소 체크상태 :
 
+ - 웹 요소 체크해제상태 :
 
+ - XPATH : /html/body/div[1]/div[4]/div[1]/div[2]/div/div[1]/div/div/div[1] + +4.상품삭제버튼 + - 웹 요소 : + 상품삭제 + + - XPATH : /html/body/div[1]/div[4]/div[1]/div[1]/div[1]/span[9] + + + +[쿠팡] +1.검색어 입력 박스 + - 웹 요소 : + - XPATH : /html/body/div[1]/div[2]/div/section/section/div/div/div[1]/div[5]/dd/div/dl[1]/dd[1]/span/table/tr[1]/td[2]/input + +2.검색버튼 + - 웹 요소 : + - XPATH : /html/body/div[1]/div[2]/div/section/section/div/div/div[1]/div[5]/dd/div/dl[2]/dd/button[2] + +3. 체크박스 + - 웹 요소 : + - XPATH : /html/body/div[1]/div[2]/div/section/section/div/div/div[1]/div[6]/div[2]/div[3]/div[1]/table/thead/tr/th[2]/span + +4.상품삭제버튼 + - 웹 요소 : + - XPATH : /html/body/div[1]/div[2]/div/section/section/div/div/div[1]/div[6]/div[2]/div[2]/div[3]/div[1]/button[3] + +[스스] +1.검색어 입력 박스 + - 웹 요소 : + - XPATH : /html/body/ui-view[1]/div[3]/div/div[3]/div/ui-view/div[2]/ui-view[1]/div[2]/form/div[1]/div/ul/li[1]/div/div/div[3]/div[1]/div[2]/div/input + +2.검색버튼 + - 웹 요소 : + - XPATH : /html/body/ui-view[1]/div[3]/div/div[3]/div/ui-view/div[2]/ui-view[1]/div[2]/form/div[2]/div/button[1] + +3. 체크박스 + - 웹 요소 : + - XPATH : /html/body/ui-view[1]/div[3]/div/div[3]/div/ui-view/div[2]/ui-view[2]/div[1]/div[2]/div[3]/div/div/div/div/div[1]/div[1]/div/div[1]/div[2]/div/label/input + +4.상품삭제버튼 + - 웹 요소 : + - XPATH : /html/body/ui-view[1]/div[3]/div/div[3]/div/ui-view/div[2]/ui-view[2]/div[1]/div[2]/div[1]/div[1]/div/div[1]/button + +[롯데온] +1.검색어 입력 박스 + - 웹 요소 : + - XPATH : /html/body/div[1]/div/div[3]/div[2]/div/div[2]/div[2]/div/div/div[3]/div/div[1]/table/tbody[1]/tr[1]/td[2]/input + +2.검색버튼 + - 웹 요소 : + - XPATH : /html/body/div[1]/div/div[3]/div[2]/div/div[2]/div[2]/div/div/div[3]/div/div[2]/input[2] + +3. 체크박스 + - 웹 요소 : + - XPATH : /html/body/div[1]/div/div[3]/div[2]/div/div[2]/div[2]/div/div/div[4]/div[2]/div[2]/div/div[1]/div/table/thead[2]/tr/th[2]/input + +4.상품삭제버튼 + - 웹 요소 : + - XPATH : /html/body/div[1]/div/div[3]/div[2]/div/div[2]/div[2]/div/div/div[4]/div[2]/div[1]/div[2]/input[8] + +[11번가] +1.검색어 입력 박스 + - 웹 요소 : + - XPATH : /html/body/div[3]/div[1]/form/div/div[2]/div[2]/div[1]/table/tbody[1]/tr[1]/td[1]/input[1] + +2.검색버튼 + - 웹 요소 : + - XPATH : /html/body/div[3]/div[1]/form/div/div[2]/div[2]/div[2]/div/button[1] + +3. 체크박스 + - 웹 요소 체크상태 : + - 웹 요소 체크해제상태 : + - XPATH : /html/body/div[3]/div[2]/div/div[2]/div/div[3]/div[1]/div/div[1]/div/div + - XPATH : /html/body/div[3]/div[2]/div/div[2]/div/div[3]/div[1]/div/div[1]/div/div/input + +4.상품삭제버튼 + - 웹 요소 : 선택상품 삭제 + - XPATH : /html/body/div[3]/div[1]/div[5]/div/a[14] + + diff --git a/web_action.py b/web_action.py index a0282ae..768040f 100644 --- a/web_action.py +++ b/web_action.py @@ -1,5 +1,5 @@ from playwright.async_api import async_playwright -import logging, random, os +import logging, random, os, time # 로거 인스턴스 가져오기 logger = logging.getLogger('default_logger') @@ -59,10 +59,28 @@ class MarketAutomation: elif market == "쿠팡": await self.page.fill('input[data-v-671ac22c][type="text"]', product_name) await self.page.click('button.searchBtn') - await self.page.wait_for_selector('span.sc-common-check input[type="checkbox"]') # 검색 결과 로드 대기 - await self.page.click('span.sc-common-check input[type="checkbox"]') + print("검색버큰 클릭") + # await self.page.wait_for_selector('#rootContainer > div:nth-child(6) > div.search-result-section > div.table-wrapper-container > div.table-wrapper.border-right.hide-see-auto-option > table > thead > tr > th:nth-child(2) > span > input[type=checkbox]') # 검색 결과 로드 대기 + # await self.page.wait_for_selector('#rootContainer > div:nth-child(6) > div.search-result-section > div.table-wrapper-container > div.table-wrapper.border-right.hide-see-auto-option > table > thead > tr > th:nth-child(2) > span > input[type=checkbox]') # 검색 결과 로드 대기 + time.sleep(2) + # 검색된상품의 텍스트가 검색상품 텍스트와 일치하는지 검사. + await self.page.wait_for_selector('#rootContainer > div:nth-child(6) > div.search-result-section > div.table-wrapper-container > div.table-wrapper.border-right.hide-see-auto-option > table > tbody > tr > td.fixed-col.left-align.small-horizontal-padding.middle-vertical-padding.border-right-double.editable-cell.editable-popup > div > span') + + # await self.page.locator('span.sc-common-check input[type="checkbox"]:nth-child(12)').click() + await self.page.click('#rootContainer > div:nth-child(6) > div.search-result-section > div.table-wrapper-container > div.table-wrapper.border-right.hide-see-auto-option > table > thead > tr > th:nth-child(2) > span > input[type=checkbox]') + print("체크박스 클릭") + + + # time.sleep(100) await self.page.wait_for_selector('button[data-v-29314869]', state='attached') - await self.page.click('button[data-v-29314869]') # 삭제 버튼 + print("삭제버튼 로드 기다림") + # CSS selector 사용 + # await self.page.locator('button.wing-web-component.wuic-button:has-text("삭제")').click() + await self.page.locator('button.wing-web-component.wuic-button:has-text("삭제"):nth-child(4)').click() + print("삭제버튼 클릭") + time.sleep(100) + # Xpath 사용 (권장하지 않음) + # await self.page.locator("xpath=/html/body/div[1]/div[2]/div/section/section/div/div/div[1]/div[6]/div[2]/div[2]/div[3]/div[1]/button[3]").click() await self.page.wait_for_load_state('networkidle') # 다음 동작 전 페이지 로딩 완료 대기