옵션 목록 입력 메서드 추가 및 기존 로직 개선. 단일 옵션 확인 타임아웃 수정, 이미지 삭제 및 추가 버튼 클릭 로직 개선. gpt_client.py에서 번역 원칙 및 예시 수정.
This commit is contained in:
parent
a70a7748ce
commit
acb94bf361
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Reference in New Issue