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 # from google.api_core.exceptions import InternalServerError # from utils import log import time import sys, re import numpy as np from naver_search import parse_naver_shopping from edit.naver_code import find_naver_code from edit.action_elements import click_element, return_element, click_and_confirm_tab from img_trans.image_trans import image_trans import logging from bs4 import BeautifulSoup import io import win32clipboard # 로거 인스턴스 가져오기 logger = logging.getLogger('default_logger') avg_price = 0 def paste_image_in_editor(driver, editor_element): action = ActionChains(driver) action.click(editor_element) # 에디터를 클릭하여 포커스를 맞춥니다. action.key_down(Keys.CONTROL).send_keys('v').key_up(Keys.CONTROL).perform() # CTRL+V를 보내 이미지를 붙여넣습니다. def image_to_clipboard(image): """ 이미지 객체(PIL.Image)를 받아 클립보드에 BMP 형식으로 복사합니다. Args: - image (PIL.Image): 이미지 객체. """ output = io.BytesIO() # BMP 형식으로 이미지를 저장합니다. BMP 형식은 클립보드에 붙여넣기를 위해 필요합니다. image.save(output, format="BMP") data = output.getvalue()[14:] # BMP 파일 헤더를 제거합니다. output.close() win32clipboard.OpenClipboard() # 클립보드를 연다 win32clipboard.EmptyClipboard() # 클립보드를 비운다 win32clipboard.SetClipboardData(win32clipboard.CF_DIB, data) # 클립보드에 이미지 데이터를 설정한다 win32clipboard.CloseClipboard() # 클립보드를 닫는다 def fetch_image_urls(html_content): """ HTML 콘텐츠에서 모든 태그의 URL을 추출합니다. 이 함수는 class="image_resized"를 가진 태그와
내부의 태그 모두를 포함합니다. """ soup = BeautifulSoup(html_content, 'html.parser') image_urls = [] # class="image_resized"를 가진 모든 태그 찾기 images_resized = soup.find_all('img', class_='image_resized') for img in images_resized: if img and 'src' in img.attrs: image_urls.append(img['src']) #
내부의 모든 태그 찾기 figures = soup.find_all('figure', class_='image') for figure in figures: img_tag = figure.find('img') if img_tag and 'src' in img_tag.attrs: image_urls.append(img_tag['src']) # 중복 제거 image_urls = list(set(image_urls)) return image_urls def safe_generate_content(gemini, product_info, max_retries=3, initial_wait=1): retry_count = 0 wait_time = initial_wait while retry_count < max_retries: try: return gemini.generate_description(product_info) # except InternalServerError as e: # logger.debug(f"Retry {retry_count + 1}/{max_retries} for InternalServerError") # time.sleep(wait_time) # wait_time *= 2 # Exponential backoff # retry_count += 1 except Exception as e: logger.debug(f"예외 발생 : {e}", exc_info=True) logger.debug(f"예외 발생으로 재시도 : {retry_count + 1}/{max_retries}") time.sleep(wait_time) wait_time *= 2 # Exponential backoff retry_count += 1 def selling_price(tao_bao_price, shipping_fee=6500, target_margin_rate=0.29, market_fee_rate=0.13, card_fee_rate=0.035, exchange_rate=200): """ 판매가를 계산하는 함수 Args: tao_bao_price: float, 타오바오 제품 원가 target_margin_rate: float, 목표 마진율 market_fee_rate: float, 마켓 수수료율 card_fee_rate: float, 카드 결제 수수료율 exchange_rate: float, 환율 shipping_fee: float, 배송비 Returns: float, 판매가 """ # 제품 원가 계산 product_cost = tao_bao_price * exchange_rate # 목표 마진 계산 target_margin = product_cost * target_margin_rate # 총 수수료율 계산 total_fee_rate = card_fee_rate + market_fee_rate # 판매가 계산 selling_price = (product_cost + target_margin) / (1 - total_fee_rate - market_fee_rate) + shipping_fee # 마켓 수수료 계산 market_fee = selling_price * market_fee_rate # 마진 계산 margin = selling_price - product_cost - market_fee - shipping_fee return selling_price, margin def extract_weight(text): """ 텍스트에서 무게 값만 추출하는 함수. 유효한 값이 없을 경우 기본값을 적용합니다. Args: text: str, 텍스트. None이거나 빈 문자열일 경우 기본값 반환. Returns: float, 추출된 무게 값 (kg) 또는 기본값. """ default_weight = 1.0 # 기본 무게값 설정 # 입력값 검증 if not text: return default_weight # 정규표현식을 사용하여 숫자와 단위를 추출합니다. pattern = r'(\d+(\.\d+)?)\s*(kg|g)' match = re.search(pattern, text) if match: weight = float(match.group(1)) # 숫자를 실수형으로 변환합니다. unit = match.group(3) # 단위를 가져옵니다. if unit == 'g': weight /= 1000 # 그램을 킬로그램으로 변환합니다. return weight if weight > 0 else default_weight else: return default_weight # 무게배송비 추정 def find_delivery_fee(weight, delv_collection): # 가장 가까운 무게값 찾기 cursor = delv_collection.find({}, {'_id': 0, 'weight': 1, 'fee': 1}).sort('weight', 1) weights = [record['weight'] for record in cursor] cursor.rewind() fees = [int(record['fee']) for record in cursor] # 콤마 제거 후 정수로 변환 # fees = [record['fee'] for record in cursor] # 입력된 무게와 가장 가까운 무게 찾기 closest_weight_index = np.argmin(np.abs(np.array(weights) - weight)) closest_weight = weights[closest_weight_index] logger.debug(f"가장 가까운 무게 : {closest_weight}") logger.debug(f"closest_weight_index: {closest_weight_index}") logger.debug(f"fees 길이 : {len(fees)}") logger.debug(f"weights 길이 : {len(weights)}") # 가장 가까운 무게에 해당하는 요금 반환 return fees[closest_weight_index] #네이버쇼핑 함수 # def NS_info(products): # # 각 상품 정보에 접근하여 텍스트로 가공 # product_info_text = "" # global avg_price # avg_price = 0 # prices = [] # for index, product in enumerate(products): # idx = index + 1 # product_info_text += '
\n' # 카드 시작 태그 # product_info_text += f"=================={idx} 번째 네이버 상품========================" # # 이미지 태그 # product_info_text += f'상품 이미지\n' # # 상품 세부 정보 태그 시작 # product_info_text += '
\n' # # 상품명 태그 # # product_info_text += f'
상품명 : {product["productTitle"]}
\n' # product_info_text += f'

상품명 : {product["productTitle"]}

\n' # # 가격 태그 # # 상품 가격을 숫자로 변환하고 포맷 # if isinstance(product["price"], str): # # 문자열인 경우 숫자로 변환 # price = int(product["price"].replace(",", "")) # 쉼표 제거 후 정수 변환 # else: # price = product["price"] # 이미 숫자인 경우 변환 없음 # prices.append(price) # formatted_price = format(price, ",") + "원" # 천 단위 구분으로 포맷 # product_info_text += f'
가격: {formatted_price}
' # # 순위 태그 # product_info_text += f'
순위: {product["rank"]}
\n' # product_info_text += "==================================================================" # # 상품 세부 정보 태그 종료 # product_info_text += '
\n' # product_info_text += '
\n' # 카드 종료 태그 # avg_price += int(product["price"]) # # avg_price = round(avg_price/len(products),0) # if products: # products가 비어 있지 않은 경우에만 평균 가격 계산 # avg_price = round(avg_price / len(products), 0) # low_price = min(prices) # high_price = max(prices) # product_info_text += f'네이버 상품의 최저 가격은 [{low_price}]' # product_info_text += f'네이버 상품의 평균 가격은 [{avg_price}]' # product_info_text += f'네이버 상품의 최고 가격은 [{high_price}]' # return product_info_text def original_html(current_html): # BeautifulSoup 객체 생성 soup = BeautifulSoup(current_html, 'html.parser') # "img class='image_resized'" 태그만 추출 resized_images = soup.find_all('img', class_='image_resized') # 추출된 태그들로 새로운 HTML 문자열 생성 original_html_text = ''.join(str(tag) for tag in resized_images) return original_html_text def NS_info_with_HTML(products): # 상품 정보를 담을 5x1 표 생성 product_info_table = "" global avg_price avg_price = 0 for product in products: if isinstance(product["price"], str): price = int(product["price"].replace(",", "")) else: price = product["price"] formatted_price = format(price, ",") + "원" # 각 상품 카드를 생성하고 테이블의 셀로 추가 product_card = create_product_card(product["productTitle"], product["imageUrl"], formatted_price, product["rank"], product["purchase"], product["review"]) product_info_table += f"" avg_price += price product_info_table += "
{product_card}
" if products: avg_price = round(avg_price / len(products), 0) product_info_table += f'

네이버 상품의 평균 가격은 {avg_price:,.0f}원입니다.

' return product_info_table def naver_prices(products): prices = [] for product in products: if product["price"] is None: logger.debug("가격 정보가 없는 제품이 있습니다.") continue # 가격 정보가 없으면, 이 제품을 건너뛰고 다음 제품으로 넘어갑니다. if isinstance(product["price"], str): price = int(product["price"].replace(",", "")) logger.debug(f"정수변환 가격 : {price}") else: price = product["price"] logger.debug(f"이미 정수인 가격 : {price}") prices.append(price) if not prices: # prices 리스트가 비어 있는 경우를 처리 logger.error("가격 정보가 없습니다.") return None, None, None low_price = min(prices) avg_price = sum(prices) / len(prices) high_price = max(prices) return low_price, avg_price, high_price def create_product_card(product_name, image_url, price, rank, purchase, review): # 카드의 크기를 150x150px로 조정 product_card = f"""
상품 이미지
{product_name}
가격: {price}
순위: {rank}
구매건수: {purchase}
리뷰수: {review}
""" return product_card # #네이버쇼핑 함수 마크다운 # def NS_info_markdown(products): # product_info_md = "" # global avg_price # avg_price = 0 # for index, product in enumerate(products): # idx = index + 1 # # 제품명을 헤더로 표시 # product_info_md += f"### {idx}번상품. {product['productTitle']}\n" # # 이미지 추가 # product_info_md += f"![상품 이미지]({product['imageUrl']})\n\n" # # 가격과 순위 정보 # if isinstance(product["price"], str): # price = int(product["price"].replace(",", "")) # 쉼표 제거 후 정수 변환 # else: # price = product["price"] # formatted_price = format(price, ",") + "원" # product_info_md += f"- **가격:** {formatted_price}\n" # product_info_md += f"- **순위:** {product['rank']}\n\n" # avg_price += price # 평균 가격 계산을 위해 가격 추가 # if products: # products가 비어 있지 않은 경우에만 평균 가격 계산 # avg_price = round(avg_price / len(products), 0) # product_info_md += f"**네이버 상품의 평균 가격은 {avg_price}원입니다.**\n" # return product_info_md def modify_detail_page(driver, product_info, gemini, delv_collection, json_naver_codes, login_info): detail_css = ".ant-tabs-tab:nth-child(6)" thumb_data_note = "5" click_and_confirm_tab(driver, thumb_data_note, 10) product_keyword = product_info.keyword_title # product_low_cost = product_info.tao_low_price # product_high_cost = product_info.tao_high_price product_low_cost = product_info.option_low_price product_high_cost = product_info.option_high_price image_src = product_info.main_image_url try: # cat = click_element(driver, 'XPATH', '//div[8]/div/div/div[2]/div/div/div/span[2]/div/div', 10, 'js') # cat = return_element(driver, 'XPATH', '//div[8]/div/div/div[2]/div/div/div/span[2]/div/div', 10) # logger.debug(f"퍼센티 등록 화면에 표시된 카테고리 텍스트 : {cat.text}") # product_info.per_cat_code = cat.text naver_code = find_naver_code(product_info.per_cat_code, json_naver_codes) logger.debug(f"검색된 스스 캣코드 : {naver_code}") product_info.naver_code = naver_code except Exception as e: logger.debug(f"카테고리 코드 검색 중 예외 발생 : {e}", exc_info=True) naver_code = "" try: logger.debug("네이버쇼핑 파싱 중") products = parse_naver_shopping(product_keyword, naver_code, isOverseas=1, sortcount=5) logger.debug("네이버쇼핑 파싱 완료") # logger.debug(f"네이버 파싱된 상품 리스트 \n {products}") product_info.naver_products = products # 네이버 상품가격 담기 naver_price = naver_prices(products) product_info.naver_low_price = naver_price[0] product_info.naver_avg_price = naver_price[1] product_info.naver_high_price = naver_price[2] # product_info_text = NS_info(products) product_info_card = NS_info_with_HTML(products) # logger.debug(f"수집된 정보 \n {product_info_card} \n") logger.debug("네이버쇼핑 파싱 완료") except Exception as e: logger.debug(f"네이버쇼핑 파싱 중 에러발생 : {e}", exc_info=True) # click_element('XPATH', '/html/body/div[7]/div/div[3]/div/div[2]/div[1]/div/div[2]/div[8]/div/div/div[2]/div[1]/div/div/span[2]/div/div') # testt = find_delivery_fee(7,delv_collection) # logger.debug(f"값 출력 : {testt}") # 상세페이지 탭으로 이동 # detail_tab = driver.find_element(By.ID, "rc-tabs-0-tab-5") try: detail_tab_xpath = ".ant-tabs-tab:nth-child(6)" detail_tab = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, ".ant-tabs-tab:nth-child(6)")) ) save_button_xpath = "//button[contains(.,'저장하기')]" save_button = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, "//button[contains(.,'저장하기')]")) ) except Exception as e: logger.debug(f"상세페이지 탭으로 이동 중 오류 발생: 요소를 찾을 수 없습니다. : {e}", exc_info=True) # detail_tab = driver.find_element(By.CSS_SELECTOR, ".ant-tabs-tab:nth-child(6)") # detail_tab.click() time.sleep(0.2) # 페이지 로딩 대기 # 상세페이지 내용 수정 try: detail_content = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.XPATH, "//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div[2]/div")) ) except Exception as e: logger.debug(f"상세페이지 xpath 요소 가져오기 오류 발생: 요소를 찾을 수 없습니다. : {e}", exc_info=True) # detail_content = driver.find_element(By.XPATH, "//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div[2]/div") # detail_content = driver.find_element(By.ID, "rc-tabs-0-tab-5") try: ActionChains(driver).move_to_element(detail_content).click().perform() time.sleep(1) logger.debug("AI 이미지 처리중") # contents = bard_img(image_src) # contents = gemini.generate_description(image_src, product_title) aicontents = safe_generate_content(gemini, product_info) logger.debug(f"{aicontents}") product_info.ai_contents = aicontents logger.debug("AI 이미지 처리 완료") weight = extract_weight(aicontents) logger.debug(f"무게 추정 : {weight}") product_info.weight = weight delv_fee = find_delivery_fee(weight, delv_collection) logger.debug(f"무게배송비 추정 : {delv_fee}") product_info.w_delv_fee = delv_fee packing_fee = 0 # 추가포장비의 경우 크기와 소재에 따라 래핑, 우드포장등 세분화 필요. 기본은 0 logger.debug(f"추가포장비 추정 : {packing_fee}") product_info.packing_fee = packing_fee except Exception as e: logger.debug(f"상세페이지 편집 중 에러발생 : {e}", exc_info=True) try: # 가격 정보 계산 low_cost, low_margin = selling_price(product_low_cost, delv_fee, 0.29) high_cost, high_margin = selling_price(product_high_cost, delv_fee, 0.29) low_delv_fee = avg_price - low_cost high_delv_fee = avg_price - high_cost # 마진율 계산 low_margin_r = round((low_margin / low_cost) * 100, 2) high_margin_r = round((high_margin / high_cost) * 100, 2) # 텍스트 조합 cost_add_text = f"""

==== 가격과 마진율 ====


해당 제품의 네이버 평균가격은 {avg_price:,.0f}원입니다.
해당 제품의 무게배송비는 {delv_fee:,.0f}원입니다.
**마진율 확보를 위한 판매가 범위:** {low_cost:,.0f}원 ~ {high_cost:,.0f}원

**저가 중심 계산:**


* 무게 배송비 대비: {low_delv_fee:,.0f}원 {('낮은' if low_delv_fee < 0 else '높은')} 편
* 마진율: {low_margin_r:,.2f}%

**고가중심 계산:**


* 무게 배송비 대비: {high_delv_fee:,.0f}원 {('낮은' if high_delv_fee < 0 else '높은')} 편
* 마진율: {high_margin_r:,.2f}%

**추가 정보:**


* 해당 제품의 파손 여부 및 무게 오차를 고려하여 더하기 마진을 설정하세요.


=======================================



""" # time.sleep(2) # detail_content.send_keys(cost_add_text) except Exception as e: logger.debug(f"가격정보 생성 중 에러 : {e}", exc_info=True) try: logger.debug("HTML 수정 버튼 클릭 .") html_btn_by_contains_xpath="//button[contains(.,'소스')]" html_btn_css = "ck.ck-button.ck-source-editing-button.ck-off.ck-button_with-text" html_btn_xpath="//div[@id='productMainContentContainerId']/div/div/div[2]/div[2]/div/div/div[2]/div/div/button[8]" click_element(driver, 'XPATH', html_btn_xpath, 5, 'ac') # html_btn = WebDriverWait(driver, 10).until( # # EC.presence_of_element_located((By.CSS_SELECTOR, ".ck-source-editing-button")) # EC.presence_of_element_located(By.CLASS_NAME, html_btn_xpath) # # EC.presence_of_element_located((By.CSS_SELECTOR, "button.ck.ck-button.ck-source-editing-button.ck-off.ck-button_with-text")) # ) # logger.debug("HTML 수정 버튼을 성공적으로 찾았습니다.") # html_btn.click() except Exception as e: logger.debug(f"HTML 수정 버튼 요소를 찾을 수 없습니다. : {e}", exc_info=True) # html_btn = driver.find_element(By.CSS_SELECTOR, ".ck-source-editing-button") try: # textarea_css = "ck-source-editing-area" # click_element(driver, 'CSS_SELECTOR', textarea_css, 10, 'js') textarea = WebDriverWait(driver, 5).until( EC.presence_of_element_located((By.CLASS_NAME, "ck-source-editing-area")) ) logger.debug("textarea버튼을 성공적으로 찾았습니다.") time.sleep(0.2) actions = ActionChains(driver) actions.move_to_element(textarea).perform() # 요소로 스크롤 logger.debug("'textarea' 요소로 스크롤 실행") time.sleep(0.2) actions.click(textarea).perform() logger.debug("'textarea' 요소 클릭 실행") # textarea.click() except Exception as e: logger.debug(f"textarea버튼 요소를 찾을 수 없습니다. : {e}", exc_info=True) try: # # data-value 속성 값을 가져옵니다. current_value = textarea.get_attribute("data-value") original_html_tags = original_html(current_value) # logger.debug(f"현재 속성값 :{current_value}") product_info.current_value = original_html_tags logger.debug("현재 속성값 수집 완료") # # ============번역을 위한 사전작업============ # 이미지 Url 수집 # translated_image_urls = [] # 번역된 이미지 URL들을 저장할 리스트 detail_images = fetch_image_urls(original_html_tags) # logger.debug(f"detail_images URLs \n {detail_images}") product_info.detail_image_urls = detail_images # 원본 URL을 빈문자열로 대체 # logger.debug("원본 URL을 빈 문자열로 대체") # for original_url in detail_images: # current_value = current_value.replace(original_url, "") # logger.debug("새로운 data-value 값으로 설정되었습니다.") # # ============번역을 위한 사전작업============ logger.debug("product_info_card 결합.") new_value = product_info_card +'

' + cost_add_text +'

' +original_html_tags # textarea.send_keys(product_info_card) time.sleep(0.2) logger.debug("결합.") # 결합된 값을 다시 textarea에 설정 try: # # data-value 속성을 새로운 값으로 설정합니다. driver.execute_script("arguments[0].setAttribute('data-value', arguments[1]);", textarea, new_value) # # driver.execute_script("arguments[0].innerHTML = arguments[1]", textarea, new_value) # # driver.execute_script("arguments[0].innerHTML = arguments[1]", detail_content, new_value); # # JavaScript를 사용하여 textarea 값 설정 # script = "arguments[0].value = arguments[1];" # driver.execute_script(script, textarea, new_value) time.sleep(0.5) logger.debug("새로운 값이 성공적으로 설정됨.") except Exception as e: logger.error(f"새로운 값 설정 중 오류 발생: {e}", exc_info=True) time.sleep(0.2) # textarea.send_keys(Keys.ENTER) # textarea.send_keys(Keys.ENTER) # logger.debug("엔터키 전송.") click_element(driver, 'XPATH', save_button_xpath, 5, 'js') time.sleep(0.2) logger.debug("저장버튼 전송.") # html_btn.click() click_element(driver, 'XPATH', html_btn_xpath, 5, 'js') logger.debug("HTML 수정 버튼 다시 클릭하여 기본편집페이지로 돌아감.") time.sleep(0.2) except Exception as e: logger.debug(f"HTML 전환 수정 중 에러발생 : {e}", exc_info=True) try: # 상세페이지 내용에 텍스트 추가 logger.debug("커서위치 맞추기") detail_content.send_keys(Keys.LEFT) detail_content.send_keys(Keys.HOME) detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.UP) detail_content.send_keys(Keys.HOME) detail_content.send_keys(Keys.HOME) # contents 변수의 값이 None이 아닐 때만 send_keys 메서드를 호출합니다. if aicontents is not None: detail_content.send_keys(aicontents) logger.debug(f"AI 컨텐츠 입력완료") else: # contents 변수가 None일 때의 대체 처리 # 예: detail_content.send_keys("") 또는 아무 동작도 수행하지 않음 pass time.sleep(0.5) detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER) detail_content.send_keys(Keys.ENTER) except Exception as e: logger.error(f"AI 컨텐츠 입력 중 에러발생 : {e}", exc_info=True) if login_info.whether_modifyImageTanslation: logger.debug("상세페이지 이미지 번역 시작") try: for i, detail_image in enumerate(detail_images): logger.debug(f"상세페이지 {i+1}번째 이미지 번역 시작") logger.debug(f"이미지 타입 : {type(detail_image)}") returned_img = image_trans(detail_image, 'translate') image_to_clipboard(returned_img) logger.debug("번역 완료 및 부텨넣기") detail_content.send_keys(Keys.CONTROL, 'v') detail_content.send_keys(Keys.ENTER) logger.debug(f"{i}번째 이미지 붙여넣기 완료") # translated_image_urls.append(returned_img_path) logger.debug("이미지 번역 완료") except Exception as e: logger.error(f"이미지 번역 중 에러발생 : {e}", exc_info=True) logger.debug("====번역이미지 붙여넣기 완료=====") logger.debug("상세페이지 편집 저장") click_element(driver, "XPATH", save_button_xpath, 5, 'js') time.sleep(0.5) logger.debug("상세페이지 편집 완료")