From 8367fbdf5a1b8f756693e2f16c1f7b266b7f8d71 Mon Sep 17 00:00:00 2001 From: R5600U_PC Date: Fri, 21 Jun 2024 00:13:44 +0900 Subject: [PATCH] =?UTF-8?q?=E3=85=85=E3=84=B7=E3=84=B4=E3=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai/gemini.py | 32 ++ edit/options.py | 4 +- edit/options_trans.py | 871 ++++++++++++++++++++++++++++++++++++++++++ edit/product_info.py | 3 + modify_products.py | 2 +- 5 files changed, 909 insertions(+), 3 deletions(-) create mode 100644 edit/options_trans.py diff --git a/ai/gemini.py b/ai/gemini.py index 0a50955..536d117 100644 --- a/ai/gemini.py +++ b/ai/gemini.py @@ -208,3 +208,35 @@ class ImageDescriptionGenerator: except Exception as e: logger.debug(f"예상치 못한 오류 발생: {e}", exc_info=True) return "예상치 못한 오류 발생: 처리할 수 없습니다." + + + def generate_option_description(self, product_info, top_k=3): + + product_info.option_datas + + prompt = f''' + 나는 타오바오에서 상품을 가져와서 한국의 온라인 쇼핑몰에서 구매대행업을 하는 사업가야. + 너는 온라인 쇼핑몰 상세페이지 제작 전문가야. 니가 만든 상품페이지는 모두를 감동시키고, 구매로 이어지는 마법같은 능력이 있어. + 여기 중국어로 된 상품의 옵션들이 있어. + 다음 옵션 목록에서 각 옵션의 고유한 특징만 추출하여 한국어로 번역하고, 최대 45바이트를 넘지 않도록 간결하게 작성해주세요. 특수 문자는 제외하고 허용되는 특수 문자(!$~()._-=+/)만 포함해주세요. + + 하나씩 차근차근 진행해보자. + + [정보] + 중국어 옵션 목록은 다음과 같아. '{product_info.option_datas}' + + [출력형식] + product_info.option_datas['name'] + [ + {"options1": "특징1"}, + {"options2": "특징2"}, + ... + ] + + options_info[f'option_type_{idx}'] = { + 'name': option_type_name, + 'items_count': option_items_count, + 'items': option_items_info + } + + ''' diff --git a/edit/options.py b/edit/options.py index 6b50cc9..c99405b 100644 --- a/edit/options.py +++ b/edit/options.py @@ -19,7 +19,7 @@ from img_trans.image_trans import image_trans # 로거 인스턴스 가져오기 logger = logging.getLogger('default_logger') -def modify_option_page2(driver, product_info, translator, login_info): +def modify_option_page2(driver, product_info, gemini, translator, login_info): # 1. 현재 옵션에 대한 정보 수집 options_info = collect_option_data(driver) @@ -50,7 +50,7 @@ def modify_option_page2(driver, product_info, translator, login_info): save_changes(driver) -def modify_option_page(driver, product_info, translator, login_info): +def modify_option_page(driver, product_info, gemini, translator, login_info): simpleMode = login_info['whether_simpleMode'] option_css = ".ant-tabs-tab:nth-child(2)" diff --git a/edit/options_trans.py b/edit/options_trans.py new file mode 100644 index 0000000..861f849 --- /dev/null +++ b/edit/options_trans.py @@ -0,0 +1,871 @@ +from selenium.webdriver.common.by import By +from selenium.webdriver.support.ui import WebDriverWait +from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.action_chains import ActionChains +from selenium.webdriver.common.keys import Keys +import time +from edit.action_elements import click_element, return_element, click_and_confirm_tab +from ai.split import parse_and_extract +from ai.deepl import trans, trans_list +from ai.deepl_with_playwright import trans_text, trans_list_text +# from ai.compare import find_most_similar_image_by_one +import re +import logging + +import tempfile +from img_trans.image_trans import image_trans + + +# 로거 인스턴스 가져오기 +logger = logging.getLogger('default_logger') + + +def modify_option_page2(driver, product_info, gemini, translator, login_info): + + # 1. 옵션타입이 단일옵션상품이 아닌지 체크 + is_option_product = not is_single_option_product(driver) + product_info.option_datas['is_option_product'] = is_option_product + + if is_option_product: + + # 2. 현재 옵션에 대한 정보 수집 + options_info = collect_option_data(driver) + product_info.option_datas['options_info'] = options_info + + # 3. 각 옵션타입별로 낮은가격순으로 정렬 + sort_options_by_price(driver, options_info) + + # 4. 미끼상품 체크 및 제외 + filtered_options_info = filter_out_bait_items(options_info) + product_info.option_datas['filtered_options_info'] = filtered_options_info + + # 5. 각 옵션타입별로 옵션아이템 가격에 따라 상품 선택 + select_valid_options(driver, filtered_options_info, login_info['whether_simpleMode']) + + # 6. 선택된 옵션들의 원본 옵션명과 옵션이미지 수집 + option_names, option_images = collect_option_names_and_images_and_prices(driver, filtered_options_info) + product_info.option_datas['option_names'] = option_names + product_info.option_datas['option_images'] = option_images + + # 7. 수집된 원본 옵션명은 AI 처리 메서드를 통해 번역 및 편집 후 입력 + update_option_names(driver, option_names, translator) + + # 8. 수집된 옵션 이미지 URL은 이미지 번역 및 업로드 과정 수행 + update_option_images(driver, option_images, translator) + + # 기타 작업이 필요한 경우 추가 + finalize_modifications(driver, product_info.option_datas) + + # 저장 작업 등 + save_changes(driver) + + +def modify_option_page(driver, product_info, gemini, translator, login_info): + simpleMode = login_info['whether_simpleMode'] + + option_css = ".ant-tabs-tab:nth-child(2)" + + thumb_data_note = "1" + click_and_confirm_tab(driver, thumb_data_note, 10) + + # 특수문자와 대체할 문자를 정의합니다. + allowed_special_chars = "!$~()._-=+/" + special_char_replacements = { + "*" : "X", # '*' 특수문자를 'X'로 대체합니다. + "【" : "(", + "】" : ")", + "[" : "(", + "]" : ")", + "," : ".", + + } + # 옵션 정보를 담을 딕셔너리 초기화 + options_info = {} + + # 옵션 탭으로 이동 + # logger.debug("옵션탭으로 이동") + # option_tab_XPATH = "//div[@id='rc-tabs-0-tab-1']" + # click_element(driver, 'XPATH', option_tab_XPATH, 10, 'js') + # logger.debug("옵션탭으로 이동 완료") + + # logger.debug("페이지 로딩 대기") + time.sleep(0.2) # 페이지 로딩 대기. + + logger.debug("옵션타입 체크") + + # # 옵션 타입 처리 + # for i in range(1, 4): # 옵션 타입이 1부터 3까지 존재한다고 가정합니다. + # option_type_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{i}]/div/div/div[2]/div/div/div[2]/div/span/input" + # option_type = return_element(driver, 'XPATH', option_type_xpath, 3) + # if option_type: + # option_type_value = option_type.get_attribute('value') + # options_info[f'option_type_{i}'] = {'options_name': option_type_value, 'options_count': 0, 'price_ranges': []} + # logger.debug(f"옵션 타입 추가: {option_type_value}") + # else: + # logger.debug(f"{option_type_xpath} 요소를 찾을 수 없음.") + + # option_type_nums = sum(1 for key in options_info.keys() if key.startswith('option_type_')) + # logger.debug(f"총 옵션 타입 수: {option_type_nums}") + + options_info = collect_option_data(driver) + logger.debug(f"수집된 옵션 정보: {options_info}") + option_type_nums = len(options_info) + + + time.sleep(30) + logger.debug(f"============================== 잠시 대기 =============================") + + # 현재 전체 옵션갯수 가져오기 + # 각 옵션 타입별로 XPath를 문자열 포맷을 사용하여 한 줄로 작성 + # option_num_xpaths = [ + # "//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{}]/div/div/div[2]/div/div/div[4]/div/div/label/span[2]".format(idx) + # for idx in range(1, 4) # 옵션 타입이 3개까지 있음 + # ] + + # 각 옵션 타입의 갯수를 가져와서 options_info에 업데이트 + # for idx, option_num_xpath in enumerate(option_num_xpaths[:option_type_nums], start=1): + # try: + # option_num_element = return_element(driver, 'XPATH', option_num_xpath, 5) + # if option_num_element: + # option_number = int(re.search(r'\d+', option_num_element.text).group()) + # logger.debug(f"옵션타입 [{idx}] 의 갯수 : {option_number}") + # options_info[f'option_type_{idx}']['options_count'] = option_number + # else: + # logger.debug(f"옵션타입 {idx} 요소를 찾을 수 없음.") + # options_info[f'option_type_{idx}']['options_count'] = None + # except Exception as e: + # logger.error(f"옵션 타입 {idx} 갯수를 가져오는 중 오류 발생: {e}", exc_info=True) + + # # 가격 낮은 순으로 정렬 + # logger.debug("가격 낮은 순으로 정렬") + # low_price_order_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[5]/div/div[3]/button/span" + # click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js') + + + ### ===================================================== ##### + ### ==이미지 비교는 정확도가 낮아 일단 보류==== ##### + # 이미지 비교 + # logger.debug("이미지 비교로 유사도 판단 후 옵션 선택") + # # 옵션 이미지 URL을 저장할 리스트 초기화 + # option_images_urls = [] + # selected_option_indexes = [] + + # for i in range(1, option_number + 1): + # # 옵션 이미지의 XPath 경로 동적 생성 + # opt_img_xpath = f"//li[{i}]/div/div/div/div[2]/div/img" + + # try: + # # 해당 XPath를 이용하여 웹 요소 찾기 + # opt_img_element = driver.find_element(By.XPATH, opt_img_xpath) + # except Exception as e: + # logger.debug(f"옵션 이미지 xpath를 찾을 수 없음 {e}", exc_info=True) + # opt_img_element = None + # # 옵션이미지가 없을 경우 처리 + # if not opt_img_element: + # logger.debug(f"{i}번째 옵션이미지가 없음") + # # 아래의 2가지 상황에 맞추어 추가코드 필요 + # # 1. 전체 옵션의 이미지가 없는 경우 처리 + # # 2. 일부 옵션만 이미지가 없는 경우 처리 + # else: + # # 웹 요소의 src 속성에서 이미지 URL 추출 + # img_url = opt_img_element.get_attribute('src') + # # 추출한 이미지 URL을 리스트에 추가 + # option_images_urls.append(img_url) + + # # 유사도 비교로 적합한 이미지를 가진 옵션번호 저장 + # # similarity = find_most_similar_image_by_one(original_img, img_url) + # similarity = 0.7 + + # if similarity > 0.6: + # selected_option_indexes.append(i) + # logger.debug(f"옵션번호 {i}번이 이미지 유사도 {similarity}로 선택되었습니다.") + + ### ===================================================== ##### + + + # # 선택된 옵션인덱스로 실제 옵션 체크 + # # 옵션타입1의 옵션전체 선택&해제 버튼 xpath=(//input[@type='checkbox'])[22] + # # 옵션타입1의 1번 옵션 선택&해제 버튼 xpath=(//input[@type='checkbox'])[23] + # # 옵션타입1의 2번 옵션 선택&해제 버튼 xpath=(//input[@type='checkbox'])[24] + # # 주의점은 해당 옵션을 체크해제할 경우 이미지 주소를 가져올 수 없으므로, 체크 해제 전 모든 옵션에 해당하는 이미지 주소를 가져올 것. + # # 만약 이미지 주소가 none이거나 없으면 이미지 비교를 하지말고 옵션명으로 비교할 것. + + # logger.debug("옵션갯수 다시 가져오기") + # selected_option_num_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[5]/div/div/label/span[2]" + # selected_option_num_element = return_element(driver, 'XPATH', selected_option_num_xpath, 10) + # selected_options_num = int(re.search(r'\d+', selected_option_num_element.text).group()) + + # logger.debug("선택된 옵션 인덱스로 옵션박스 체크 실행") + + # 각 옵션 타입에 대한 처리 + for option_idx in range(1, option_type_nums + 1): + try: + logger.debug(f"옵션타입 {option_idx}에 대한 처리 시작") + logger.debug("가격 낮은 순으로 정렬") + # low_price_order_xpath = f"//div[{option_idx}]/div/div/div[2]/div/div/div[4]/div[2]/div[2]/div/div[4]/button" + # low_price_order_css = f"div#productMainContentContainerId div:nth-child(2) > div > button[type=\"button\"]:nth-child(3)" + low_price_order_css = f"div#productMainContentContainerId div:nth-child({option_idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-cAkrUM.flTgaK > div.ant-row.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(2) > div > button[type=\"button\"]:nth-child(3)" + # click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js') + click_element(driver, 'CSS_SELECTOR', low_price_order_css, 10, 'js') + if not simpleMode: # 심플모드일 경우 옵션 편집하지 않음. 옵션 번역만. + edit_option(driver, option_idx, options_info[f'option_type_{option_idx}']['options_count']) # 각 옵션의 전체 체크박스 해제 + # 가격 낮은 순으로 재정렬 + # logger.debug("가격 낮은 순으로 재정렬") + # click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js') + option_name_trans(driver, translator, product_info, option_idx, options_info[f'option_type_{option_idx}']['options_count'], allowed_special_chars, special_char_replacements) + except KeyError: + logger.error(f"옵션타입 {option_idx}에 대한 정보가 없습니다.") + except Exception as e: + logger.error(f"옵션타입 {option_idx} 처리 중 오류 발생: {e}", exc_info=True) + + + logger.debug("옵션선택과 옵션명 수정 완료 후 옵션명 정리") + # 빈칸제거 + # xpath=//span[contains(.,'빈칸 제거')] + space_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div/div/div/div[3]/button/span" + click_element(driver, 'XPATH', space_xpath, 10, 'normal') + + # A-Z + # xpath=//span[contains(.,'A-Z')] + AtoZ_xpath="//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div/div/div/div/button/span" + if not simpleMode: # 심플모드일 경우 옵션 AtoZ 편집하지 않음. 옵션 번역만. + click_element(driver, 'XPATH', AtoZ_xpath, 10, 'normal') + + # 1-99 + # xpath=//span[contains(.,'1-99')] + # xpath=//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div/div/div/div[2]/div/div/div[4]/div/div/div/div[2]/button/span + + logger.debug("옵션 가격범위를 product_info에 업데이트") + update_price_range(driver, options_info, product_info) + + save_xpath="//button[contains(.,'저장하기')]" + click_element(driver, 'XPATH', save_xpath, 10, 'js') + logger.debug("옵션 정리 후 저장버튼 클릭 완료") + +def edit_option(driver, option_type, option_count): + """ + 주어진 옵션 타입에 대해 옵션 편집 동작을 수행합니다. + + Parameters: + - driver: WebDriver 인스턴스 + - option_type: 옵션 타입 (1 또는 2) + - option_count: 옵션 타입 내의 옵션 갯수 + """ + try: + # 전체 체크박스 선택 해제 + logger.debug("전체 체크박스 선택 해제") + # select_all_checkbox_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type}]/div/div/div[2]/div/div/div[5]/div/div/label/span" + select_all_checkbox_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]" + + # logger.debug("전체체크박스의 상태확인") + # select_all_checkbox_state_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]/input" + # select_all_checkbox_state_element = return_element(driver, 'XPATH', select_all_checkbox_state_xpath, 10) + # # select_all_checkbox_state_element = driver.find_element_by_xpath(select_all_checkbox_state_xpath) + # if select_all_checkbox_state_element.get_attribute("aria-checked") == "mixed": + # logger.debug("전체체크박스의 일부 선택상태 확인") + # click_element(driver, 'XPATH', select_all_checkbox_xpath, 10, 'js') + + click_element(driver, 'XPATH', select_all_checkbox_xpath, 10, 'js') + logger.debug(f"옵션타입 {option_type} 전체 선택 체크박스 해제") + time.sleep(1) + + logger.debug(f"옵션타입 {option_type}의 옵션 선택 시작") + + if option_count <= 2: + # 옵션 갯수가 2개 이하면 모든 옵션 체크 + for i in range(1, option_count + 1): + option_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[1]/label/span" + click_element(driver, 'XPATH', option_xpath, 10, 'ac') + logger.debug(f"옵션 {i} 체크") + time.sleep(0.5) + else: + # 옵션 갯수가 3개 이상인 경우, 첫 번째 옵션 제외 최대 5개 옵션 체크 + for i in range(2, min(option_count, 6) + 1): # 첫 번째 옵션 제외, 최대 5개 선택 + option_xpath = f"//div[{option_type}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[1]/label/span" + click_element(driver, 'XPATH', option_xpath, 10, 'ac') + logger.debug(f"옵션 {i} 체크") + time.sleep(0.5) + + except Exception as e: + logger.error(f"옵션 타입 {option_type} 편집 중 예외 발생: {e}", exc_info=True) + +def option_name_trans(driver, translator, product_info, option_type_number, option_count, allowed_special_chars, special_char_replacements): + + """ + 원본 옵션명을 수집하여 번역 후 새로 입력 + + Parameters: + - param driver: WebDriver 인스턴스 + - param options_info : 옵션 정보 Dict + - param allowed_special_chars: 허용되는 특수문자 (기본값: "!$~()._-=+/") + - type allowed_special_chars: str + - param special_char_replacements: 특수문자 대체 규칙 (기본값: None) + - type special_char_replacements: dict + """ + + logger.debug("선택된 옵션명 DeepL로 번역 후 새로운 옵션명 입력") + ori_optionNames = [] + deepl_trans_optionNames = [] + # 원본 옵션명 추출 및 이미지 URL 확인 + try: + for i in range(1, option_count + 1): + if option_type_number == 1: + ori_optionName_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[3]/span" + optionImage_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type_number}]/div/div[2]/div/div[{i}]/div/div[2]/div/img" + deleteButton_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type_number}]/div/div[2]/div/div[{i}]/div/div[2]/div/span" + elif option_type_number == 2: + ori_optionName_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div[3]/span" + else: # option_type_number == 3 + ori_optionName_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div[4]/div/span" + + ori_optionName_element = driver.find_element(By.XPATH, ori_optionName_xpath) + ori_optionName = ori_optionName_element.text.strip() + cleaned_ori_optionName = replace_or_remove_special_chars(ori_optionName, allowed_special_chars, special_char_replacements) + logger.debug(f"정제된 {i}번째 원본 옵션명 : {cleaned_ori_optionName}") + ori_optionNames.append(cleaned_ori_optionName) + + # 1번 옵션타입의 이미지를 번역하고 업로드 + if option_type_number == 1: + try: + option_image_element = return_element(driver, 'XPATH', optionImage_xpath, 3) + if option_image_element: + option_image_url = option_image_element.get_attribute('src') + logger.debug(f"옵션 이미지 URL : {option_image_url}") + translated_image = image_trans(option_image_url, translator, 'translate', logger) + if translated_image: + # 이미지 번역 후 업로드 + temp_file_path = save_image_to_tempfile(translated_image) + # 이미지 삭제 버튼 클릭 + click_element(driver, 'XPATH', deleteButton_xpath, 5, 'ac') + logger.debug(f"이미지 삭제 버튼 클릭 완료") + # 새로운 이미지 업로드 + upload_image(driver) + else: + logger.debug(f"옵션 이미지 번역 실패: {option_image_url}") + else: + logger.debug(f"{i}번째 옵션에 이미지 없음") + except Exception as e: + logger.error(f"옵션 이미지 처리 중 오류 발생: {e}", exc_info=True) + + except Exception as e: + logger.debug(f"원본옵션명 처리중 에러발생 : {e}", exc_info=True) + + + + # 원본 옵션명을 하나의 텍스트로 합치기 + # combined_ori_optionNames = '\n\n'.join(ori_optionNames) + + # logger.debug("원본 텍스트의 특수문자 제거 및 대체") + # cleaned_ori_text = replace_or_remove_special_chars(combined_ori_optionNames, allowed_special_chars, special_char_replacements) + # logger.debug(f"원본옵션명 집합 \n {cleaned_ori_text}") + + logger.debug(f"원본옵션 총 {len(ori_optionNames)}개 번역 시행") + deepl_trans_optionNames = translator.translate(ori_optionNames) # DeepL 번역 함수 + + # deepl_trans_optionNames = trans_list_text(ori_optionNames) # DeepL 번역 함수 + #deepl_trans_optionNames = trans_list(ori_optionNames) # DeepL 번역 함수 + # trans_optionNames_text = trans_text(cleaned_ori_text) # DeepL_with_playwright 번역 함수 + + # trans_optionNames_text = trans(cleaned_ori_text) # DeepL 번역 함수 + + logger.debug(f"번역된 텍스트 \n {deepl_trans_optionNames} ") + + logger.debug("번역 텍스트의 특수문자 제거 및 대체") + trans_optionNames = [] + for deepl_trans_optionName in deepl_trans_optionNames: + cleand_trans_optionName = replace_or_remove_special_chars(deepl_trans_optionName, allowed_special_chars, special_char_replacements) + trans_optionNames.append(cleand_trans_optionName) + + parsed_trans_optionNames, common_names = parse_and_extract(trans_optionNames) + # trans_optionNames = trans_optionNames_text.split('\n\n') + # logger.debug("번역 텍스트 나누기") + + logger.debug("product_info 옵션명 업데이트") + try: + product_info.update_option_names(option_type_number, ori_optionNames, parsed_trans_optionNames, common_names) + except Exception as e: + logger.debug(f"product_info 옵션명 업데이트 중 에러 발생 {e}", exc_info=True) + + # 번역된 옵션명을 각 input 요소에 입력 + try: + logger.debug(f"parsed_trans_optionNames 갯수 : {len(parsed_trans_optionNames)}") + for i, trans_optionName in enumerate(parsed_trans_optionNames, 1): + optionName_input_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[2]/div[1]/span/input" if option_type_number != 2 else f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[2]/div[2]/div[1]/span/input" + optionName_input_element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.XPATH, optionName_input_xpath))) + + logger.debug("마우스 액션체인으로 클릭하여 안정적인 포커스 이동") + ActionChains(driver).click(optionName_input_element).perform() + logger.debug("기존 텍스트 클리어") + optionName_input_element.send_keys(Keys.CONTROL + "a") # 전체 선택 + optionName_input_element.send_keys(Keys.DELETE) # 삭제 + time.sleep(0.5) + logger.debug("번역 텍스트 sendkey") + optionName_input_element.send_keys(trans_optionName) # 번역된 옵션명 입력 + logger.debug(f"{i}번째 옵션명에 '{trans_optionName}' 입력") + except Exception as e: + logger.debug(f"번역 옵션명 처리중 에러발생 : {e}", exc_info=True) + + +# 특수문자를 대체하거나 제거하는 함수를 정의합니다. +def replace_or_remove_special_chars(text, allowed_special_chars="!$~()._-=+/", special_char_replacements=None): + """ + 주어진 문자열에서 특정 특수문자를 대체하거나 제거합니다. + + :param text: 대상 문자열 + :type text: str + :param allowed_special_chars: 허용되는 특수문자 (기본값: "!$~()._-=+/") + :type allowed_special_chars: str + :param special_char_replacements: 특수문자 대체 규칙 (기본값: None) + :type special_char_replacements: dict + :return: 특수문자가 대체 또는 제거된 문자열 + :rtype: str + """ + # 특수문자 대체 규칙을 설정합니다. + if special_char_replacements is None: + special_char_replacements = {} + + # 특수문자를 대체합니다. + for char, replacement in special_char_replacements.items(): + text = text.replace(char, replacement) + + # 허용되는 특수문자 패턴을 정의합니다. + pattern = f"[^{re.escape(allowed_special_chars)}\w\s]" + + # 허용되는 특수문자를 제외하고 모든 특수문자를 제거합니다. + cleaned_text = re.sub(pattern, "", text, flags=re.UNICODE) + + return cleaned_text + +def update_price_range(driver, options_info, product_info): + try: + # 모든 옵션 타입에서 수집된 가격 범위를 저장할 리스트 초기화 + all_price_ranges = [] + + # 각 옵션 타입에 대한 처리 + for option_type_key, option_data in options_info.items(): + option_number = option_data.get('options_count') + + # 각 옵션의 가격 범위 수집 + for i in range(1, option_number + 1): + option_type_number = [int(s) for s in option_type_key.split('_') if s.isdigit()][0] + middle_index = '3' if option_type_number == 1 else '2' + price_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[{middle_index}]/div[1]/div[2]/span/sup" + try: + price_element = return_element(driver, 'XPATH', price_xpath, 5) + price_text = re.sub(r'[^\d\-]', '', price_element.text.strip()) + price_parts = price_text.split('-') + price_range = [int(price.strip()) for price in price_parts] + all_price_ranges.extend(price_range) + except Exception as e: + logger.error(f"옵션 타입 {option_type_key}의 {i}번 가격 범위를 가져오는데 실패했습니다. 에러: {e}", exc_info=True) + + # 모든 옵션의 가격 범위에서 전체 최소값과 최대값 추출 + if all_price_ranges: + overall_min_price = min(all_price_ranges) + overall_max_price = max(all_price_ranges) + + # product_info 업데이트 + product_info.option_high_price = overall_max_price + product_info.option_low_price = overall_min_price + + logger.debug(f"전체 옵션의 가격 범위 업데이트: 최소값 {overall_min_price}, 최대값 {overall_max_price}") + except Exception as e: + logger.error(f"가격 범위 업데이트 중 오류 발생: {e}", exc_info=True) + + + +def save_image_to_tempfile(image): + """ + 이미지를 임시 파일로 저장합니다. + + Args: + - image (PIL.Image): 번역된 이미지 객체 + + Returns: + - temp_file_path (str): 임시 파일 경로 + """ + temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png") + image.save(temp_file, format="PNG") + temp_file_path = temp_file.name + temp_file.close() + logger.debug(f"임시 파일로 저장된 이미지 경로: {temp_file_path}") + return temp_file_path + + +def upload_image(driver, option_item_index, temp_file_path): + """ + 번역된 이미지를 업로드합니다. + + Args: + - driver: WebDriver 인스턴스 + - option_item_index (int): 옵션 아이템의 인덱스 (1, 2, 3 등) + - temp_file_path (str): 임시 파일 경로 + """ + try: + # 기존 이미지 삭제 버튼 클릭 + delete_button_css = f"div#productMainContentContainerId li:nth-child({option_item_index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div.ant-row.ant-row-no-wrap.ant-row-space-between.ant-row-middle.css-1li46mu > div:nth-child(1) > div > span" + delete_button_element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, delete_button_css)) + ) + delete_button_element.click() + logger.debug(f"{option_item_index}번째 옵션 아이템의 이미지 삭제 버튼 클릭 완료") + + time.sleep(1) # 이미지 삭제 대기 + + # 이미지 업로드 버튼 클릭 + upload_button_css = f"div#productMainContentContainerId li:nth-child({option_item_index}) > div > div:nth-child(1) > div > div:nth-child(2) > div > div > img" + upload_button_element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, upload_button_css)) + ) + upload_button_element.click() + logger.debug(f"{option_item_index}번째 옵션 아이템의 이미지 업로드 버튼 클릭 완료") + + # 이미지 업로드 다이얼로그의 파일 업로드 버튼 클릭 + file_upload_dialog_css = "span > div > div > div:nth-child(2) > div" + file_upload_dialog_element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, file_upload_dialog_css)) + ) + file_upload_dialog_element.click() + logger.debug("파일 업로드 버튼 클릭 완료") + + # 파일 선택 대화상자에 임시 이미지 파일 경로를 입력하여 업로드 + driver.find_element(By.CSS_SELECTOR, 'input[type="file"]').send_keys(temp_file_path) + logger.debug(f"임시 이미지 파일 경로 지정: {temp_file_path}") + + # 이미지 삽입 버튼 클릭 + insert_button_css = "div.ant-modal-footer > button[type=\"button\"].ant-btn.css-1li46mu.ant-btn-primary" + insert_button_element = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, insert_button_css)) + ) + insert_button_element.click() + logger.debug("이미지 삽입 버튼 클릭 완료") + + # 임시 파일 삭제 + os.remove(temp_file_path) + logger.debug(f"임시 파일 삭제 완료: {temp_file_path}") + + except Exception as e: + logger.error(f"이미지 업로드 중 에러 발생: {e}", exc_info=True) + + +def collect_option_data_ori(driver): + """ + 웹페이지에서 옵션 타입의 갯수와 각 타입별 옵션 아이템의 갯수를 수집합니다. + + Parameters: + - driver: WebDriver 인스턴스 + + Returns: + - filtered_options_info (dict): 옵션 타입과 각 타입별 옵션 아이템 갯수를 포함하는 필터링된 딕셔너리 + """ + options_info = {} + + try: + # 먼저 상품 유형을 확인합니다. + if not is_option_product(driver): + logger.debug("단일 상품이므로 옵션 정보 수집을 생략합니다.") + return options_info # 단일 상품일 경우 빈 딕셔너리를 반환 + + # 옵션 타입 컨테이너의 CSS 선택자 + option_types_container_css = "div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2)" + + # 옵션 타입 컨테이너 요소를 찾습니다. + option_types_container = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, option_types_container_css)) + ) + + # 옵션 타입 갯수를 추출하기 위한 CSS 선택자 + option_type_css = "div.ant-col.css-1li46mu > div.sc-stxIr.eeUZbv" + option_type_elements = option_types_container.find_elements(By.CSS_SELECTOR, option_type_css) + option_type_count = len(option_type_elements) + + logger.debug(f"옵션 타입의 총 갯수: {option_type_count}") + + for idx in range(1, option_type_count + 1): + # 옵션 타입 이름 추출 + # option_type_name_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{idx}]/div/div/div[2]/div/div/div[2]/div/span/input" + + # option_type_name_element = WebDriverWait(driver, 10).until( + # EC.presence_of_element_located((By.XPATH, option_type_name_xpath)) + # ) + # option_type_name = option_type_name_element.get_attribute('value').strip() + + + option_type_name_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.ant-row.ant-row-no-wrap.ant-row-middle.css-1li46mu > div:nth-child(1) > span > input" + option_type_name__by_css_element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, option_type_name_css)) + ) + option_type_name = option_type_name__by_css_element.get_attribute('value').strip() + + # 옵션 타입별 옵션 아이템 갯수 추출 + # option_items_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{idx}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li" + # option_items = driver.find_elements(By.CSS_SELECTOR, option_items_xpath) + # option_items_count = len(option_items) + + option_items_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li" + option_items = driver.find_elements(By.CSS_SELECTOR, option_items_css) + option_items_count = len(option_items) + + # 각 옵션 타입별 옵션명, 이미지 및 가격을 수집 + option_names_and_images = collect_option_names_and_images_and_price(driver, idx, option_items_count) + + options_info[f'option_type_{idx}'] = { + 'name': option_type_name, + 'items_count': option_items_count, + 'items': option_names_and_images + } + + logger.debug(f"옵션 타입 {idx}: {option_type_name}, 옵션 아이템 수: {option_items_count}") + for item in option_names_and_images: + logger.debug(f" - 옵션 아이템: 이름={item['name']}, 이미지 URL={item['image_url']}, 가격={item['low_price']} - {item['high_price']}") + + except Exception as e: + logger.error(f"옵션 타입 및 아이템 수집 중 오류 발생: {e}", exc_info=True) + + return options_info + +def collect_option_data(driver, product_info): + options_info = {} + + # 옵션 타입을 찾는 CSS 선택자 + option_types_container_css = "div#productMainContentContainerId div.ant-col.css-1li46mu div.sc-stxIr.eeUZbv" + option_type_elements = driver.find_elements(By.CSS_SELECTOR, option_types_container_css) + + # 각 옵션 타입을 순회하며 정보 수집 + for idx, option_type_element in enumerate(option_type_elements, start=1): + try: + # 옵션 타입 이름 추출 + option_type_name_css = f"div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2) > div > div:nth-child({idx}) > div > div > div.ant-row.ant-row-no-wrap.ant-row-middle.css-1li46mu > div:nth-child(1) > span > input" + option_type_name_element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, option_type_name_css)) + ) + option_type_name = option_type_name_element.get_attribute('value').strip() + + # 옵션 아이템을 찾는 CSS 선택자 + option_items_css = f"div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2) > div > div:nth-child({idx}) div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li" + option_items_elements = driver.find_elements(By.CSS_SELECTOR, option_items_css) + option_items_count = len(option_items_elements) + + # 옵션 아이템 정보 수집 + option_items_info = [] + for item_idx, item_element in enumerate(option_items_elements, start=1): + try: + # 옵션 이름 추출 + name_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({item_idx}) > div > div.ant-col.css-1li46mu > div > div.ant-col.css-1li46mu > span" + name_element = item_element.find_element(By.CSS_SELECTOR, name_css) + item_name = name_element.text.strip() + + # 옵션 이미지 URL 추출 + image_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({item_idx}) > div > div.ant-col.css-1li46mu > div > div > div > img" + image_element = item_element.find_element(By.CSS_SELECTOR, image_css) + image_url = image_element.get_attribute('src') if image_element else None + + # 옵션 가격 범위 추출 + price_css = f"div#productMainContentContainerId div:nth-child({idx}) > div > div > div.ant-collapse-content.ant-collapse-content-active > div > div > div.sc-kjNGdX.cQdCox > div.ant-list.ant-list-split.css-1li46mu > div > div > ul > li:nth-child({item_idx}) > div > div.ant-col.css-1li46mu > div > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > span > sup" + price_element = item_element.find_element(By.CSS_SELECTOR, price_css) + price_text = price_element.get_attribute('title').strip() + + # 가격 범위 추출 및 처리 + price_range = re.findall(r'\d+', price_text.replace(',', '')) + price_range = (int(price_range[0]), int(price_range[1])) if len(price_range) == 2 else (int(price_range[0]), int(price_range[0])) + + # 옵션 정보 저장 + option_items_info.append({ + 'name': item_name, + 'image_url': image_url, + 'price_range': price_range + }) + except Exception as e: + logger.error(f"옵션 아이템 수집 중 오류 발생: {e}", exc_info=True) + + # 옵션 타입 정보 저장 + options_info[f'option_type_{idx}'] = { + 'name': option_type_name, + 'items_count': option_items_count, + 'items': option_items_info + } + + except Exception as e: + logger.error(f"옵션 타입 및 아이템 수집 중 오류 발생: {e}", exc_info=True) + + product_info.option_datas = options_info + return options_info + + +def collect_option_names_and_images_and_price(driver, option_type_number, option_items_count): + """ + 각 옵션 타입별로 원본 옵션명, 옵션 이미지, 옵션 가격을 수집합니다. + + Parameters: + - driver: WebDriver 인스턴스 + - option_type_number: 옵션 타입 번호 (1, 2, 3 등) + - option_items_count: 해당 옵션 타입의 옵션 아이템 갯수 + + Returns: + - option_data (list): 옵션명, 이미지 URL, 가격을 포함하는 리스트 + """ + option_data = [] + + try: + for i in range(1, option_items_count + 1): + # 원본 옵션명 수집 + option_name_xpath = f"//div[@id='productMainContentContainerId']/div/div[2]/div/div/div[2]/div/div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i}]/div/div[1]/div/div[3]/div[3]/span" + option_name_element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.XPATH, option_name_xpath)) + ) + option_name = option_name_element.text.strip() + + # 옵션 이미지 URL 수집 (옵션 타입 1에만 옵션이미지 존재) + option_image_url = None + if option_type_number == 1: + option_image_xpath = f"div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(2) > div > img" + try: + option_image_element = driver.find_element(By.CSS_SELECTOR, option_image_xpath) + option_image_url = option_image_element.get_attribute('src') + except Exception: + logger.debug(f"{option_type_number}번 옵션타입의 {i}번째 옵션에 이미지 없음") + + # 옵션 가격 수집 + option_price_xpath = f"div#productMainContentContainerId li:nth-child({i}) > div > div:nth-child(1) > div > div:nth-child(3) > div:nth-child(1) > div:nth-child(2) > span > sup" + try: + option_price_element = driver.find_element(By.CSS_SELECTOR, option_price_xpath) + option_price_text = option_price_element.get_attribute('title') # 또는 .text를 사용할 수도 있음 + # 가격 문자열에서 숫자만 추출 (천 단위 구분자를 포함하는 경우 처리) + price_range = re.findall(r'\d+', option_price_text.replace(',', '')) + low_price, high_price = map(int, price_range) if len(price_range) == 2 else (int(price_range[0]), int(price_range[0])) + except Exception: + logger.debug(f"{option_type_number}번 옵션타입의 {i}번째 옵션에 가격 없음") + low_price = high_price = None + + option_data.append({ + 'name': option_name, + 'image_url': option_image_url, + 'low_price': low_price, + 'high_price': high_price + }) + logger.debug(f"{option_type_number}번 옵션타입의 옵션 {i}: 이름={option_name}, 이미지 URL={option_image_url}, 가격={low_price} - {high_price}") + + except Exception as e: + logger.error(f"옵션명, 이미지 및 가격 수집 중 오류 발생: {e}", exc_info=True) + + return option_data + +def is_single_option_product(driver, product_info): + """ + 웹페이지에서 현재 상품이 '단일 상품'인지 '옵션 상품'인지 확인합니다. + + Parameters: + - driver: WebDriver 인스턴스 + + Returns: + - bool: 옵션 상품이면 True, 단일 상품이면 False + """ + try: + # '옵션 상품등록' 또는 '단일 상품등록' 선택 여부 확인 + radio_group_css = "div#productMainContentContainerId div.ant-row.css-1li46mu > div" + radio_group_element = WebDriverWait(driver, 10).until( + EC.presence_of_element_located((By.CSS_SELECTOR, radio_group_css)) + ) + + # 라디오 버튼의 선택 상태 확인 + option_radio_xpath = "//label[span[text()='옵션 상품등록']]//input[@type='radio' and @value='false']" + single_radio_xpath = "//label[span[text()='단일 상품등록']]//input[@type='radio' and @value='true']" + + option_radio_checked = radio_group_element.find_element(By.XPATH, option_radio_xpath).is_selected() + single_radio_checked = radio_group_element.find_element(By.XPATH, single_radio_xpath).is_selected() + + if option_radio_checked and not single_radio_checked: + logger.debug("상품 유형: 옵션 상품등록") + product_info.option_types_onlyone = True + return True + else: + logger.debug("상품 유형: 단일 상품등록") + product_info.option_types_onlyone = False + return False + + except Exception as e: + logger.error(f"상품 유형 확인 중 오류 발생: {e}", exc_info=True) + logger.debug("상품 유형 오류 발생으로 기본설정인 단일 상품등록으로 진행합니다.") + product_info.option_types_onlyone = False + return False # 오류 발생 시 기본적으로 단일 상품으로 처리 + +def filter_and_select_options(driver, option_type_number, option_data): + """ + 미끼 옵션을 제거하고 유효한 옵션을 선택합니다. + + Parameters: + - driver: WebDriver 인스턴스 + - option_type_number: 옵션 타입 번호 (1, 2, 3 등) + - option_data (list): 옵션명, 이미지 URL, 가격을 포함하는 리스트 + """ + try: + # 가격 데이터만 추출하여 정렬 + prices = sorted([item['low_price'] for item in option_data if item['low_price'] is not None]) + if not prices: + logger.error("가격 데이터가 없습니다.") + return + + # 미끼 옵션을 식별하여 제거 + valid_items = [] + for i, base_price in enumerate(prices): + range_limit = base_price * 1.5 + within_range = [price for price in prices if price <= range_limit] + if len(within_range) == len(prices): + valid_items = [item for item in option_data if item['low_price'] in within_range] + break + + if not valid_items: + valid_items = option_data + + # 전체 체크박스 해제 + select_all_checkbox_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[4]/div[2]/div[1]/label/span[1]" + WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.XPATH, select_all_checkbox_xpath)) + ).click() + time.sleep(1) + + # 필터링된 옵션을 낮은 가격순으로 정렬 + valid_items = sorted(valid_items, key=lambda x: x['low_price']) + + # 최대 10개 옵션만 선택, 옵션이 5개 이하라면 모든 옵션 선택 + for i, item in enumerate(valid_items): + if i >= 10: + break + option_checkbox_xpath = f"//div[{option_type_number}]/div/div/div[2]/div/div/div[5]/div[1]/div/div/ul/li[{i + 1}]/div/div[1]/div/div[1]/label/span[1]" + WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.XPATH, option_checkbox_xpath)) + ).click() + time.sleep(0.5) + + logger.debug(f"옵션 타입 {option_type_number}의 유효한 옵션 아이템 선택 완료") + + except Exception as e: + logger.error(f"옵션 선택 중 오류 발생: {e}", exc_info=True) + +def sort_options_by_price(driver, options_info): + """ + 옵션 타입별로 가격 내림차순 정렬 버튼을 클릭합니다. + + Parameters: + - driver: WebDriver 인스턴스 + - options_info: 옵션 타입별 정보를 포함하는 딕셔너리 + """ + try: + for option_type_key in options_info: + option_type_number = option_type_key.split('_')[-1] # 옵션 타입 번호 추출 + sort_button_css = f"div#productMainContentContainerId div.sc-bCrHVQ.fRmCVg > div:nth-child(2) > div > div > div:nth-child(2) > div > div:nth-child({option_type_number}) > div > div > div:nth-child(2) > div > div > div:nth-child(4) > div:nth-child(2) > div:nth-child(2) > div > div:nth-child(4) > button" + + sort_button = WebDriverWait(driver, 10).until( + EC.element_to_be_clickable((By.CSS_SELECTOR, sort_button_css)) + ) + sort_button.click() + logger.debug(f"옵션 타입 {option_type_number}의 가격 내림차순 정렬 버튼 클릭 완료") + time.sleep(1) # 정렬이 완료될 시간을 줍니다. + + # low_price_order_css = "div#productMainContentContainerId div:nth-child(2) > div > button[type=\"button\"]:nth-child(3)" + # # click_element(driver, 'XPATH', low_price_order_xpath, 10, 'js') + # click_element(driver, 'CSS_SELECTOR', low_price_order_css, 10, 'js') + + except Exception as e: + logger.error(f"가격 내림차순 정렬 버튼 클릭 중 오류 발생: {e}", exc_info=True) diff --git a/edit/product_info.py b/edit/product_info.py index 17f0627..0f5f087 100644 --- a/edit/product_info.py +++ b/edit/product_info.py @@ -31,6 +31,9 @@ class ProductInfo: self.naver_products = [] # 네이버 파싱된 상품들 + self.is_option_product = False + self.option_datas = {} + self.option_1_names = [] # 상품 옵션 이름 리스트 self.trans_option_1_names = [] # 번역된 상품 옵션 이름 리스트 self.trans_option_1_name_common_parts = [] # 번역된 상품 옵션 이름 리스트의 단어별 리스트 저장 diff --git a/modify_products.py b/modify_products.py index 9a6f242..d3247e2 100644 --- a/modify_products.py +++ b/modify_products.py @@ -197,7 +197,7 @@ def modify_products(driver, gemini, translator, mongo_config, login_info, set_nu steps_and_actions = [ ('tag_modification', edit_tag, (driver, product_infos[i-1])), ('thumbnail_modification', modify_thumb_page, (driver, product_infos[i-1])), - ('option_modification', modify_option_page, (driver, product_infos[i-1], translator, login_info)), + ('option_modification', modify_option_page, (driver, product_infos[i-1], gemini, translator, login_info)), ('detail_page_modification', modify_detail_page, (driver, product_infos[i-1], gemini, translator, delv_collection, json_naver_codes, login_info)), ('price_modification', modify_price_page, (driver, product_infos[i-1])), ('title_modification', modify_product_title, (driver, product_infos[i-1], login_info)),