수정 중

This commit is contained in:
R5600U_PC 2024-05-13 16:19:57 +09:00
parent 753960ccd3
commit 50cc450fe8
3 changed files with 212 additions and 13 deletions

51
main.py
View File

@ -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):

148
prompt.txt Normal file
View File

@ -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.검색어 입력 박스
- 웹 요소 : <textarea id="txtKeyword" cols="10" rows="10" title="상품명 / SKU명 / 브랜드명 / 제조사명 / 관리코드 / SKU번호 / SKU관리코드 / ESM상품식별코드명 / ESM상품식별코드번호">상품명 / SKU명 / 브랜드명 / 제조사명 / 관리코드 / SKU번호 / SKU관리코드 / ESM상품식별코드명 / ESM상품식별코드번호</textarea>
- XPATH : /html/body/div[1]/div[3]/div[3]/div/div[1]/div[1]/dl[1]/dd/div/textarea
2.검색버튼
- 웹 요소 : <a href="javascript:;">
<img src="https://pics.esmplus.com/front/btn/btn_search1.gif" alt="검색하기" id="imgItemsSearch">
</a>
- XPATH : /html/body/div[1]/div[3]/div[3]/div/div[3]/div[2]/a[1]
3. 체크박스
- 웹 요소 체크상태 : <div id="gridcolumn-1014" class="x-unselectable x-column-header-checkbox x-column-header-align-left x-box-item x-column-header x-unselectable-default x-column-header-first" style="border-width: 1px; width: 24px; height: auto; left: 0px; top: 0px; margin: 0px;"><div id="gridcolumn-1014-titleEl" class="x-column-header-inner" style="height: auto; padding-top: 5px;"><span id="gridcolumn-1014-textEl" class="x-column-header-text">&nbsp;</span></div><div id="gridcolumn-1014-clearEl" class="x-clear" role="presentation"></div></div>
- 웹 요소 체크해제상태 : <div id="gridcolumn-1014" class="x-unselectable x-column-header-checkbox x-column-header-align-left x-box-item x-column-header x-unselectable-default x-column-header-first x-grid-hd-checker-on" style="border-width: 1px; width: 24px; height: auto; left: 0px; top: 0px; margin: 0px;"><div id="gridcolumn-1014-titleEl" class="x-column-header-inner" style="height: auto; padding-top: 5px;"><span id="gridcolumn-1014-textEl" class="x-column-header-text">&nbsp;</span></div><div id="gridcolumn-1014-clearEl" class="x-clear" role="presentation"></div></div>
- XPATH : /html/body/div[1]/div[4]/div[1]/div[2]/div/div[1]/div/div/div[1]
4.상품삭제버튼
- 웹 요소 : <span class="css_btn1">
<a href="javascript:;" onclick="javascript:ItemMngPopup.Open('SellStateDelete');">상품삭제</a>
</span>
- XPATH : /html/body/div[1]/div[4]/div[1]/div[1]/div[1]/span[9]
[쿠팡]
1.검색어 입력 박스
- 웹 요소 : <input data-v-671ac22c="" type="text" placeholder="두 글자 이상 입력" data-sc-common-uicfg="size:small" class="sc-common-input inputHeight32 inputWidth224">
- 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.검색버튼
- 웹 요소 : <button data-v-671ac22c="" data-wuic-props="name:btn size:m type:primary" class="wing-web-component button searchBtn" style="margin-left: 8px;">
검색
</button>
- XPATH : /html/body/div[1]/div[2]/div/section/section/div/div/div[1]/div[5]/dd/div/dl[2]/dd/button[2]
3. 체크박스
- 웹 요소 : <span data-v-774563c0="" class="sc-common-check"><input data-v-774563c0="" type="checkbox"><i data-v-774563c0=""></i></span>
- 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.상품삭제버튼
- 웹 요소 : <button data-v-29314869="" data-v-1b622923="" type="button" data-wuic-props="name:btn" class="wing-web-component wuic-button"><!---->
삭제
<!----></button>
- 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.검색어 입력 박스
- 웹 요소 : <input type="text" class="form-control ng-pristine ng-untouched ng-valid ng-empty ng-valid-pattern ng-valid-maxlength" id="prd_name" name="productName" ng-model="vm.searchFormData.productName" maxlength="100" maxlength-err-type="max.product.productName" ng-pattern="/^[^\\*?&quot;<>]+$/" ng-pattern-err-type="invalidSpecialCharacter" ncp-message-container="#error_searchKeyword_productName" ncp-input-clear="">
- 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.검색버튼
- 웹 요소 : <button type="submit" class="btn btn-primary" ng-click="vm.viewData.selectedProductStatusType = undefined" data-nclicks-code="sgn.search">검색</button>
- 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. 체크박스
- 웹 요소 : <input type="checkbox" class="ag-selection-checkbox" data-nclicks-code="itg.allcheck">
- 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.상품삭제버튼
- 웹 요소 : <button type="submit" class="btn btn-default btn-sm" ng-click="vm.func.changeProductStatus('DELETE')" data-nclicks-code="itl.delete">선택삭제</button>
- 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.검색어 입력 박스
- 웹 요소 : <input id="mf_tac_layout_contents_ML000000055_body_ibx_spdNm" style="width:150px;" class="w2input wq_ipt" type="text">
- 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.검색버튼
- 웹 요소 : <input type="button" tabindex="0" id="mf_tac_layout_contents_ML000000055_body_btn_trigger7" class="w2trigger btn_cm point search " value="조회">
- 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. 체크박스
- 웹 요소 : <input type="checkbox" name="wq_uuid_1420_header__column9_checkboxLabel_" id="wq_uuid_1420_header__column9_checkboxLabel__id" colid="">
- 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.상품삭제버튼
- 웹 요소 : <input type="button" tabindex="0" id="mf_tac_layout_contents_ML000000055_body_btn_delSpd" class="w2trigger btn_cm sm point3" value="선택상품삭제">
- 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.검색어 입력 박스
- 웹 요소 : <input id="prdNm" name="prdNm" type="text" class="text" value="" style="width: 160px;" onclick="setPrdNm(); doCommonStat('NPSB004');" onkeypress="goEnterCheck();" onkeydown="if(event.keyCode == 13){return false;}">
- 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.검색버튼
- 웹 요소 : <button type="button" class="defbtn_lar ladtype defbtn_seh" id="btnSearch" onclick="prdNoCheck();"><span><span>검색</span></span></button>
- XPATH : /html/body/div[3]/div[1]/form/div/div[2]/div[2]/div[2]/div/button[1]
3. 체크박스
- 웹 요소 체크상태 : <div style="cursor: pointer; margin-left: 5px; top: 50%; margin-top: -8px; position: relative; width: 16px; height: 16px;" id="jqxWidget530c321e" role="checkbox" aria-checked="true" aria-disabled="false"><div class="jqx-checkbox-default jqx-fill-state-normal jqx-rc-all"><div style="width: 13px; height: 13px;"><span style="width: 13px; height: 13px;" class="jqx-checkbox-check-checked"></span></div></div><input type="hidden" value="true"></div>
- 웹 요소 체크해제상태 : <div style="cursor: pointer; margin-left: 5px; top: 50%; margin-top: -8px; position: relative; width: 16px; height: 16px;" id="jqxWidget530c321e" role="checkbox" aria-checked="false" aria-disabled="false"><div class="jqx-checkbox-default jqx-fill-state-normal jqx-rc-all"><div style="width: 13px; height: 13px;"><span style="width: 13px; height: 13px;" class=""></span></div></div><input type="hidden" value="false"></div>
- 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.상품삭제버튼
- 웹 요소 : <a href="javascript:fnListDelete('U');" onclick="doCommonStat('NPOF017');" class="defbtn_lsm dtype3"><span>선택상품 삭제</span></a>
- XPATH : /html/body/div[3]/div[1]/div[5]/div/a[14]

View File

@ -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') # 다음 동작 전 페이지 로딩 완료 대기