옵션 목록 입력 메서드 추가 및 기존 로직 개선. 단일 옵션 확인 타임아웃 수정, 이미지 삭제 및 추가 버튼 클릭 로직 개선. gpt_client.py에서 번역 원칙 및 예시 수정.

This commit is contained in:
Envy_PC 2025-07-13 20:30:36 +09:00
parent a70a7748ce
commit acb94bf361
7 changed files with 226 additions and 138 deletions

0
-i Normal file
View File

View File

@ -3,7 +3,7 @@ from PySide6.QtCore import QThread, Signal, QMutex, QWaitCondition, Slot
import re
# import pyautogui
import time
import win32gui, win32con
# import win32gui, win32con
import asyncio
import os, sys, random
import requests

View File

@ -273,6 +273,38 @@ class DetailHandler:
self.logger.log(f"이미지 업로드 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return False
async def input_option_list(self, option_data):
"""옵션 목록을 입력하는 메서드"""
try:
input_field = await self.page.wait_for_selector(self.locator_manager.get_locator('detail_page', 'detail_input_field'), timeout=10000)
# 옵션 목록 헤더 입력
await input_field.type("# 옵션 목록")
await input_field.press('Enter')
# 첫 번째 옵션 입력
first_key = list(option_data.keys())[0]
await input_field.type(f"- 1. {first_key}")
await input_field.press('Enter')
# 나머지 옵션 입력
for i, key in enumerate(list(option_data.keys())[1:], start=2):
await input_field.type(f"- {i}. {key}")
await input_field.press('Enter')
# 목록 종료 및 후두부 텍스트 입력
await input_field.press('Enter')
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
await input_field.press('Enter')
await input_field.type('---')
await input_field.press('Enter')
self.logger.info('옵션 목록 입력 완료')
except Exception as e:
self.logger.error(f"옵션 목록 입력 중 오류 발생: {str(e)}")
raise
async def input_detail_text(self, optionHandler):
"""
상세페이지에 소개글과 옵션 데이터를 입력하는 메서드
@ -298,37 +330,22 @@ class DetailHandler:
self.logger.log(f"텍스트 입력 완료: {leading_text}", level=logging.DEBUG)
# 옵션 데이터 입력 (단일 옵션이 아닌 경우에만)
# option_data = optionHandler.get_selected_translated_options()
option_data = optionHandler.get_all_translated_options() # 모든 옵션 입력
self.logger.log(f"DetailHandler | option_data : {option_data}", level=logging.DEBUG)
# 옵션 데이터가 있는 경우 옵션 목록 입력
if option_data and len(option_data) > 0:
is_single = optionHandler.option_info.get('is_single_option', True)
if not is_single:
self.logger.log('단일옵션이 아니므로 옵션목록을 입력', level=logging.INFO)
# 옵션 목록 헤더 입력
await input_field.type("# 옵션 목록")
await input_field.press('Enter')
# 첫 번째 옵션 입력
first_key = list(option_data.keys())[0]
await input_field.type(f"- 1. {first_key}")
await input_field.press('Enter')
# 나머지 옵션 입력
for i, key in enumerate(list(option_data.keys())[1:], start=2):
await input_field.type(f"- {i}. {key}")
await input_field.press('Enter')
# 목록 종료 및 후두부 텍스트 입력
await input_field.press('Enter')
await input_field.type('### 나열된 옵션목록 이외의 옵션이 필요하실 경우 고객센터로 연락주세요.')
await input_field.press('Enter')
await input_field.type('---')
await input_field.press('Enter')
self.logger.log('옵션 데이터 입력 완료', level=logging.INFO)
# 옵션이 2개 이상이면 무조건 다중 옵션으로 처리
if len(option_data) > 1:
self.logger.info(f"다중 옵션 감지 ({len(option_data)}개), 옵션 목록 입력 시작")
await self.input_option_list(option_data)
else:
# 옵션이 1개인 경우에만 is_single 값 확인
is_single = optionHandler.option_info.get('is_single_option', True)
if not is_single:
self.logger.info("단일 옵션이 아님, 옵션 목록 입력 시작")
await self.input_option_list(option_data)
else:
self.logger.info("단일 옵션 상품으로 판단, 옵션 목록 입력 건너뜀")
except Exception as e:
self.logger.log(f"상세페이지 텍스트 입력 중 오류 발생: {e}", level=logging.ERROR)

View File

@ -1,6 +1,5 @@
import numpy as np
import asyncio, time, math
import pywinauto
import os
import logging
import random
@ -552,7 +551,7 @@ class OptionHandler:
# self.logger.log(f"단일 옵션 확인 중 예외 발생: {e}", level=logging.ERROR, exc_info=True)
# return False
async def is_single_option(self, timeout=3000):
async def is_single_option(self, timeout=1500):
"""
단일 상품 상태 여부를 확인하는 메서드 (ant-alert-content 요소 탐지, timeout 적용)
:param timeout: 요소 탐지 최대 대기 시간(ms)
@ -569,7 +568,7 @@ class OptionHandler:
except Exception as e:
# 타임아웃 또는 요소 미발견: 옵션상품이거나 알 수 없음
self.logger.log(
f"단일 옵션 확인 중 예외 발생 또는 타임아웃(옵션 상품 추정): {e}",
f"단일 옵션 확인 시간이 지났으므로 옵션상품임: {e}",
level=logging.DEBUG
)
return False
@ -958,13 +957,17 @@ class OptionHandler:
self.option_info.setdefault("translated_names", {}).update(all_translated_names)
self.logger.log(f"translated_names 일괄 업데이트: {all_translated_names}", level=logging.DEBUG)
self.logger.log(f"option_info: {self.option_info}", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"번역된 옵션명을 입력하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
def get_all_translated_options(self):
"""클래스 변수에 저장된 모든 번역된 옵션명을 반환"""
return self.option_info.get('translated_names', {})
"""클래스 변수에 저장된 모든 번역된 옵션명을 반환 (번역명만 키로 사용)"""
translated_names = self.option_info.get('translated_names', {})
# 원본명:번역명 구조를 번역명:번역명 구조로 변환
return {translated_name: translated_name for translated_name in translated_names.values()}
def _round_to_UP(self, number, nearest=1000):
"""
@ -1284,30 +1287,43 @@ class OptionHandler:
async def click_option_image_delete_button(self):
dialogs = await self.page.query_selector_all('[role="dialog"][aria-modal="true"]')
for dialog in dialogs:
"""
옵션 이미지 삭제 확인 다이얼로그에서 삭제 버튼을 클릭합니다.
Playwright 요소 체인을 사용하여 다이얼로그 "삭제" 텍스트 버튼을 찾습니다.
"""
try:
# 삭제 확인 다이얼로그가 나타날 때까지 대기
self.logger.log("옵션 이미지 삭제 확인 다이얼로그 대기 중...", level=logging.DEBUG)
await self.page.wait_for_selector('div.ant-modal-confirm[role="dialog"]', timeout=5000)
self.logger.log("옵션 이미지 삭제 확인 다이얼로그 발견!", level=logging.DEBUG)
# 다이얼로그 내용 확인 (선택사항)
try:
content = await dialog.inner_text()
dialog_content = await self.page.locator('div.ant-modal-confirm[role="dialog"]').inner_text()
self.logger.log(f"다이얼로그 내용: {dialog_content}", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"dialog.inner_text() 에러: {e}", level=logging.ERROR)
continue
self.logger.log(f"다이얼로그 내용 확인 실패: {e}", level=logging.WARNING)
if "옵션 이미지를 삭제하시겠습니까?" in content:
self.logger.log("옵션이미지 삭제 다이알로그 발견!", level=logging.DEBUG)
try:
delete_button = await dialog.query_selector("button:has-text('삭제')")
if delete_button:
await delete_button.click()
self.logger.log("삭제버튼 클릭 성공!", level=logging.DEBUG)
return True
else:
self.logger.log("삭제버튼을 찾지 못했습니다.", level=logging.ERROR)
return False
except Exception as e:
self.logger.log(f"삭제버튼 클릭 중 에러: {e}", level=logging.ERROR)
return False
self.logger.log("옵션이미지 삭제 다이알로그를 찾지 못했습니다.", level=logging.ERROR)
# 삭제 버튼이 활성화될 때까지 대기 후 클릭
self.logger.log("삭제 버튼 활성화 대기 중...", level=logging.DEBUG)
delete_button = self.page.locator('div.ant-modal-confirm[role="dialog"]').locator('button:has-text("삭제")')
await delete_button.wait_for(timeout=3000)
await asyncio.sleep(0.2) # 버튼 활성화를 위한 짧은 대기
await delete_button.click()
self.logger.log("삭제 버튼 클릭 성공!", level=logging.DEBUG)
# 다이얼로그가 사라질 때까지 대기
try:
await self.page.wait_for_selector('div.ant-modal-confirm[role="dialog"]', state='detached', timeout=3000)
self.logger.log("삭제 확인 다이얼로그가 정상적으로 닫혔습니다.", level=logging.DEBUG)
return True
except Exception as e:
self.logger.log(f"다이얼로그 닫힘 확인 중 오류: {e}", level=logging.WARNING)
return False
except Exception as e:
self.logger.log(f"삭제 버튼 클릭 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
return False
async def get_latest_delete_button(self, page, option_idx):
@ -1332,25 +1348,46 @@ class OptionHandler:
async def update_option_image_to_other(self, toggle_states):
"""
현재 DOM 상태 기준으로 제외되지 않은 옵션만 이미지 번역/업로드 (option_type_1만)
옵션타입1의 옵션아이템들을 Playwright 체인으로 정확히 처리하여 이미지 번역/업로드
"""
try:
# 현재 DOM 상태 기준으로 옵션 아이템들 조회
item_selector = "div#productMainContentContainerId div.ant-collapse-content-box li.ant-list-item[aria-roledescription='sortable']"
current_option_items = await self.page.query_selector_all(item_selector)
# 옵션타입 컨테이너들 조회 (첫 번째가 옵션타입1)
option_type_containers = await self.page.locator("div#productMainContentContainerId div.ant-collapse-content-box").all()
if not option_type_containers:
self.logger.log("옵션타입 컨테이너를 찾을 수 없습니다.", level=logging.ERROR)
return
# 옵션타입1 선택 (첫 번째 컨테이너)
option_type_1 = option_type_containers[0]
self.logger.log("옵션타입1 컨테이너 선택 완료", level=logging.DEBUG)
# 옵션타입1 내부의 옵션아이템들 조회
option_items = await option_type_1.locator("li.ant-list-item[aria-roledescription='sortable']").all()
if not option_items:
self.logger.log("옵션타입1에 옵션아이템이 없습니다.", level=logging.INFO)
return
# 제외되지 않고 이미지가 있는 옵션만 필터링
valid_items = []
for item_elem in current_option_items:
text_content = await item_elem.inner_text()
if "제외된 옵션" not in text_content:
# 이미지 존재 확인
img_elem = await item_elem.query_selector("img")
if img_elem:
img_url = await img_elem.get_attribute("src")
# SVG 이미지 제외
if not img_url.endswith(".svg"):
valid_items.append(item_elem)
for item in option_items:
# 제외된 옵션인지 확인
text_content = await item.inner_text()
if "제외된 옵션" in text_content:
continue
# 이미지 존재 확인 (SVG 제외)
img_elements = await item.locator("img").all()
has_valid_image = False
for img in img_elements:
src = await img.get_attribute("src")
if src and not src.endswith(".svg"):
has_valid_image = True
break
if has_valid_image:
valid_items.append(item)
total_options = len(valid_items)
self.logger.log(f"{total_options}개의 유효한 옵션에 대해 이미지 번역을 시작합니다.", level=logging.DEBUG)
@ -1362,14 +1399,24 @@ class OptionHandler:
translated_index = 1
self.set_progress_visible_signal.emit(True)
for idx, option_elem in enumerate(valid_items, start=1):
for idx, option_item in enumerate(valid_items, start=1):
try:
# 이미지 URL 가져오기
option_image = await option_elem.query_selector("img")
option_image_url = await option_image.get_attribute("src")
# 옵션 이미지 URL 가져오기 (SVG 제외한 첫 번째 이미지)
img_elements = await option_item.locator("img").all()
option_image_url = None
for img in img_elements:
src = await img.get_attribute("src")
if src and not src.endswith(".svg"):
option_image_url = src
break
if not option_image_url:
self.logger.log(f"{idx}번째 옵션에 유효한 이미지가 없습니다.", level=logging.WARNING)
continue
self.logger.log(f"{idx}번째 옵션 이미지 URL: {option_image_url}", level=logging.DEBUG)
# 이미지 번역 처리 (반드시 dict 리턴)
# 이미지 번역 처리
self.logger.log(f"{idx}번째 옵션의 이미지 처리 시도", level=logging.DEBUG)
result = await self.imageProcessor.process_single_image(
page=self.page,
@ -1391,16 +1438,12 @@ class OptionHandler:
self.logger.log(f"{idx}번째 옵션의 이미지 처리 완료: {translated_image_path}", level=logging.DEBUG)
# 삭제 버튼 클릭 - 현재 옵션 요소에서 직접 찾기
# 삭제 버튼 클릭 - 옵션아이템 내부에서 FootnoteDescription 클래스의 '삭제' 텍스트 찾기
self.logger.log(f"{idx}번째 옵션의 이미지 삭제 버튼 클릭 시도", level=logging.DEBUG)
try:
delete_btn_elem = await self.find_delete_button_in_option(option_elem)
if not delete_btn_elem:
self.logger.log(f"{idx}번째 옵션의 삭제 버튼이 없어 클릭을 생략합니다.", level=logging.WARNING)
continue
await delete_btn_elem.click()
delete_button = option_item.locator("span.FootnoteDescription:has-text('삭제')")
await delete_button.wait_for(timeout=3000)
await delete_button.click()
self.logger.log(f"{idx}번째 옵션의 삭제 버튼 클릭", level=logging.DEBUG)
# 삭제 확인 다이얼로그 처리
@ -1413,31 +1456,31 @@ class OptionHandler:
except Exception as e:
self.logger.log(f"{idx}번째 옵션의 삭제 버튼을 찾는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
# 이미지 추가(+버튼) - 현재 옵션 요소에서 직접 찾기
# 이미지 추가 버튼 클릭 - 옵션아이템 내부에서 ic_image_add.svg 이미지 찾기
try:
add_btn_elem = await self.find_add_button_in_option(option_elem)
add_image_button = option_item.locator("img[src='./ic_image_add.svg']")
await add_image_button.wait_for(timeout=3000)
await add_image_button.click()
self.logger.log(f"{idx}번째 옵션의 이미지 추가 버튼 클릭", level=logging.DEBUG)
# 파일 업로드 다이얼로그에서 업로드 버튼 찾기
upload_button = self.page.locator("span.ant-upload-btn")
await upload_button.wait_for(timeout=5000)
if not add_btn_elem:
self.logger.log(f"{idx}번째 옵션의 이미지추가 버튼을 찾을 수 없습니다.", level=logging.ERROR)
continue
# 파일 업로드 input 찾기 (ant-upload-btn 내부 또는 상위)
file_input = self.page.locator("input[type='file']")
await file_input.set_input_files(translated_image_path)
self.logger.log(f"{idx}번째 옵션의 파일 업로드 완료", level=logging.DEBUG)
await add_btn_elem.click()
self.logger.log(f"{idx}번째 옵션의 이미지추가 버튼 클릭", level=logging.DEBUG)
# 파일업로드 input
file_input = await self.page.query_selector(self.file_upload_button_selector)
if file_input:
await file_input.set_input_files(translated_image_path)
self.logger.log(f"{idx}번째 옵션의 파일 업로드 완료", level=logging.DEBUG)
# '이미지 삽입' 버튼 클릭
confirm_upload_button = await self.page.wait_for_selector(self.confirm_upload_button_selector)
await confirm_upload_button.click()
self.logger.log(f"{idx}번째 옵션에 이미지가 업로드되었습니다.", level=logging.DEBUG)
else:
self.logger.log(f"{idx}번째 옵션의 파일 입력 요소를 찾을 수 없습니다.", level=logging.ERROR)
continue
# '이미지 삽입' 버튼 대기 및 클릭
confirm_upload_button = self.page.locator(self.confirm_upload_button_selector)
await confirm_upload_button.wait_for(timeout=5000)
await confirm_upload_button.click()
self.logger.log(f"{idx}번째 옵션에 이미지가 업로드되었습니다.", level=logging.DEBUG)
# 업로드 완료 대기
await asyncio.sleep(0.5)
except Exception as e:
self.logger.log(f"{idx}번째 옵션의 이미지를 추가하는 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)

View File

@ -1,6 +1,6 @@
import base64
import pyperclip
import win32clipboard
# import base64
# import pyperclip
# import win32clipboard
from io import BytesIO
from PIL import Image, ImageGrab, ImageFont, ImageDraw
import requests

View File

@ -6,7 +6,7 @@ import logging
class GPTClient:
def __init__(self, logger, supabase_manager, user_id, model="gpt-4o-mini", temperature=0.2):
def __init__(self, logger, supabase_manager, user_id, model="gpt-4o-mini", temperature=0.0):
self.logger = logger
self.supabase_manager = supabase_manager
self.user_id = user_id
@ -287,31 +287,52 @@ class GPTClient:
for attempt in range(max_retry):
prompt = (
f"다음은 옵션의 특징만 간단하게 남긴 후 번역해야 할 원래 옵션 이름들입니다: {json.dumps(cleaned_data, ensure_ascii=False)}.\n\n"
f"원래 제품 이름은 '{product_name}'이며, 번역 시 이를 참조하여 제품의 특징을 선별해야 합니다.\n\n"
"### 번역 규칙:\n"
"1. 각 옵션은 고유한 특징을 유지하면서 최소한의 길이로 만드세요. 다만 아무리 길어도 최대25자(공백포함)를 넘으면 안됩니다.\n"
"2. 각 옵션의 고유한 특징은 크기, 무게, 재질, 사이즈, 용량, 전압, 전류 또는 제품 코드를 말한다.\n"
"3. **모든 번역 결과는 반드시 한국어만 사용하고, 영어, 숫자를 제외한 외국어는 절대 포함하지 마세요. 한글 외 문자가 남아있으면 오답입니다.**\n"
"4. 번역 후 모든 옵션에 공통된 단어들은 제거하세요.\n"
"5. 번역 후 옵션 이름이 중복될 경우, 원래 옵션 이름에서 추가적인 고유 특징을 추출하여 구별되도록 하세요.\n"
"6. 고객 서비스 문의, 가격 문의, 견적 또는 예약을 요청하는 옵션 이름은 제거하세요.\n"
"7. 의미가 일치하는 경우 긴 단어를 짧은 단어로 대체하세요 (예: 'Display Panel''Screen'으로 대체).\n"
"8. 번역된 옵션 이름은 다음과 같은 JSON 형식으로 반환하세요:\n\n"
f"당신은 중국 상품 옵션명을 한국 온라인 쇼핑몰용으로 번역하는 전문가입니다.\n"
f"장황한 옵션명을 핵심 특징만 남겨서 간결하게 만들어야 합니다.\n\n"
f"번역할 옵션 데이터: {json.dumps(cleaned_data, ensure_ascii=False)}\n"
f"제품명: {product_name}\n\n"
"### 번역 원칙:\n"
"1. **핵심 특징 추출**: 세대/버전, 색상, 주요 기능만 남기고 나머지는 제거\n"
"2. **길이 제한**: 각 옵션명은 15자 이내로 제한\n"
"3. **고유값 보존**: 숫자, 영문은 그대로 유지 (예: 8대→8세대, A타입)\n"
"4. **공통어 완전 제거**: 모든 옵션에 공통으로 들어가는 단어는 완전히 제거\n"
"5. **괄호 활용**: 세부 기능은 괄호로 구분하여 간결하게 표현\n\n"
"### 번역 과정:\n"
"1단계: 각 옵션에서 핵심 특징만 추출 (세대, 색상, 주요기능)\n"
"2단계: 공통 단어 완전 제거 (강력, 모터, 브러시 등)\n"
"3단계: 괄호를 활용해 세부 기능 구분\n"
"4단계: 15자 이내로 최종 정리\n\n"
"### 번역 예시:\n"
"입력: {\n"
" '8代太空舱白【强力全壁刷】【强力去污】强力电机': '8대 우주선 흰색 강력 전면 브러시 강력 세척 강력 모터',\n"
" '8代太空舱黑【强力全壁刷】【强力去污】升级电机': '8대 우주선 검정 강력 전면 브러시 강력 세척 업그레이드 모터'\n"
"}\n\n"
"핵심 특징 추출:\n"
"- 공통어: '우주선', '강력', '모터', '브러시' 제거\n"
"- 세대: 8대→8세대\n"
"- 색상: 흰색, 검정\n"
"- 기능: 전면, 업그레이드\n\n"
"최종 출력: {\n"
' "trans_option_1": "8세대 흰색(전면)",\n'
' "trans_option_2": "8세대 검정(전면)"\n'
"}\n\n"
"### 출력 형식:\n"
"반드시 JSON 형식으로 응답하세요:\n"
"{\n"
" \"trans_option_1\": \"번역된 옵션 이름 1\",\n"
" \"trans_option_2\": \"번역된 옵션 이름 2\",\n"
" \"trans_option_3\": \"번역된 옵션 이름 3\",\n"
" \"trans_option_4\": \"번역된 옵션 이름 4\"\n"
"}\n"
"9. **각 옵션의 고유값(숫자, 영어, 특수문자 등)은 반드시 번역 결과에도 그대로 남겨두세요. 예: '150x180''150x180', 'A타입''A타입'**\n"
"번역 결과에 한글이 아닌 문자가 포함되어 있다면 반드시 한글로 다시 번역하여 결과에 반영하세요.\n"
"예시: \n"
"- 잘못된 번역: \"청색(蓝色)\" → 올바른 번역: \"파랑\"\n"
"- 잘못된 번역: \"大号\" → 올바른 번역: \"대형\"\n"
"- 잘못된 번역: \"型号123\" → 올바른 번역: \"123형\"\n"
"- 잘못된 번역: \"红色\" → 올바른 번역: \"빨강\"\n"
"반드시 모든 옵션이 한글만 포함된 상태로 반환되도록 검증하세요."
' "trans_option_1": "번역된 옵션명 1",\n'
' "trans_option_2": "번역된 옵션명 2",\n'
' "trans_option_3": "번역된 옵션명 3"\n'
"}\n\n"
"### 번역 예시:\n"
"입력: {'红色大号强力款': '빨간 대형 강력형', '蓝色小号强力款': '파란 소형 강력형'}\n"
"공통어 제거: '강력형' 제거\n"
"최종 출력: {'trans_option_1': '빨간 대형', 'trans_option_2': '파란 소형'}"
)
self.logger.log("GPT 모델에 프롬프트를 전달하여 응답을 기다리는 중...", level=logging.DEBUG)
@ -332,16 +353,23 @@ class GPTClient:
for i, gpt_key in enumerate(gpt_keys):
src = original_names[i]
tgt = gpt_response[gpt_key]
src_features = self.extract_key_features(src)
# 원본 옵션명에서 핵심 특징들을 추출하여 번역 결과에 포함되어 있는지 확인
import re
# 숫자, 영문자, 특수문자 조합을 핵심 특징으로 추출
key_features = re.findall(r'[a-zA-Z0-9]+(?:[x×]\d+)*|[0-9]+(?:\.[0-9]+)?(?:[a-zA-Z]+)?', src)
match_count = 0
for f in src_features:
if self.is_feature_in_text(f, tgt):
for feature in key_features:
if self.is_feature_in_text(feature, tgt):
match_count += 1
# feature 중 1개 이상만 일치해도 OK
if src_features and match_count == 0:
# 핵심 특징이 있는 경우 최소 1개 이상은 번역 결과에 포함되어야 함
if key_features and match_count == 0:
is_order_valid = False
self.logger.log(f"[경고] 옵션 고유값 부분 불일치: '{src}'의 주요값이 번역 결과 '{tgt}'에 없음", level=logging.WARNING)
self.logger.log(f"[경고] 옵션 고유값 부분 불일치: '{src}'의 주요값 {key_features}이 번역 결과 '{tgt}'에 없음", level=logging.WARNING)
break
if not is_order_valid:
self.logger.log("[경고] 옵션 순서/고유값 불일치! 재시도...", level=logging.WARNING)
continue