세션 관리 기능 개선: 비활성 세션 정리 기능을 추가하여 사용자 경험을 향상시켰습니다. 그룹 목록 새로고침 버튼을 추가하고, UI 요소를 개선하여 사용자의 편의성을 높였습니다. 옵션명 정리 로직을 수정하여 번역된 값을 우선적으로 사용하도록 변경하였습니다. 데이터베이스 파일이 업데이트되었습니다.
This commit is contained in:
parent
9cdd2dcc64
commit
78541df3b2
|
|
@ -998,19 +998,22 @@ class BrowserController(QThread):
|
|||
self.browser_ad2_close_error.emit(str(e))
|
||||
return
|
||||
|
||||
try:
|
||||
# 그룹 이름 리스트 가져오기
|
||||
# group_names_list = await self.get_group_names_list_by_index()
|
||||
group_names_list = await self.get_group_names_list_all()
|
||||
# if "전체" in group_names_list:
|
||||
# group_names_list.remove("전체")
|
||||
# group_names_list.insert(0, "전체")
|
||||
self.group_list_signal.emit(group_names_list)
|
||||
except Exception as e:
|
||||
self.logger.log(f"그룹 이름 리스트 가져오기 오류: {str(e)}", level=logging.ERROR, exc_info=True)
|
||||
screenshot_path = await self.save_error_screenshot()
|
||||
self.browser_group_list_error.emit(str(e))
|
||||
return
|
||||
# try:
|
||||
# # 그룹 이름 리스트 가져오기
|
||||
# # group_names_list = await self.get_group_names_list_by_index()
|
||||
# group_names_list = await self.get_group_names_list_all()
|
||||
# # if "전체" in group_names_list:
|
||||
# # group_names_list.remove("전체")
|
||||
# # group_names_list.insert(0, "전체")
|
||||
# self.group_list_signal.emit(group_names_list)
|
||||
# except Exception as e:
|
||||
# self.logger.log(f"그룹 이름 리스트 가져오기 오류: {str(e)}", level=logging.ERROR, exc_info=True)
|
||||
# screenshot_path = await self.save_error_screenshot()
|
||||
# self.browser_group_list_error.emit(str(e))
|
||||
# return
|
||||
self.logger.log("작업그룹 목록 가져오기 시작...", level=logging.INFO)
|
||||
await self.get_group_names_list_all_async()
|
||||
self.logger.log("작업그룹 목록 가져오기 완료...", level=logging.INFO)
|
||||
|
||||
try:
|
||||
# 각 핸들러에 초기화된 page 객체 전달.
|
||||
|
|
@ -1113,6 +1116,22 @@ class BrowserController(QThread):
|
|||
self.unknown_browser_error.emit(str(e))
|
||||
return
|
||||
|
||||
async def get_group_names_list_all_async(self):
|
||||
"""그룹 이름 목록 가져오기 비동기 작업"""
|
||||
try:
|
||||
# 그룹 이름 리스트 가져오기
|
||||
# group_names_list = await self.get_group_names_list_by_index()
|
||||
group_names_list = await self.get_group_names_list_all()
|
||||
# if "전체" in group_names_list:
|
||||
# group_names_list.remove("전체")
|
||||
# group_names_list.insert(0, "전체")
|
||||
self.group_list_signal.emit(group_names_list)
|
||||
except Exception as e:
|
||||
self.logger.log(f"그룹 이름 리스트 가져오기 오류: {str(e)}", level=logging.ERROR, exc_info=True)
|
||||
screenshot_path = await self.save_error_screenshot()
|
||||
self.browser_group_list_error.emit(str(e))
|
||||
return
|
||||
|
||||
def ensure_chrome_prefs_disable_password_leak(self, user_data_dir: str) -> None:
|
||||
"""
|
||||
브라우저 실행 전 Preferences 파일을 직접 수정하여 보안 경고를 원천 차단합니다.
|
||||
|
|
@ -2334,37 +2353,60 @@ class BrowserController(QThread):
|
|||
await self.page.wait_for_selector(self.dropdown_openstatus_locator, timeout=10000)
|
||||
self.logger.log("드롭다운이 열렸습니다.", level=logging.DEBUG)
|
||||
|
||||
# 드롭다운이 완전히 렌더링될 때까지 대기
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
group_names = []
|
||||
seen = set()
|
||||
first_item = None
|
||||
consecutive_repeats = 0 # 연속된 반복 횟수 추적
|
||||
|
||||
# 첫 번째 항목 포커스 (ArrowDown으로 시작)
|
||||
await self.page.keyboard.press("ArrowDown")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
for _ in range(200): # 최대 200개 그룹 가정
|
||||
# 드롭다운이 열릴 때 이미 활성화된 첫 번째 항목을 먼저 읽기
|
||||
current_option = await self.page.evaluate("""
|
||||
() => {
|
||||
const active = document.querySelector('.ant-select-item-option-active .ant-select-item-option-content');
|
||||
return active ? active.textContent.trim() : null;
|
||||
}
|
||||
""")
|
||||
if current_option and current_option not in seen:
|
||||
|
||||
if current_option:
|
||||
first_item = current_option
|
||||
seen.add(current_option)
|
||||
group_names.append(current_option)
|
||||
self.logger.log(f"발견: {current_option}", level=logging.DEBUG)
|
||||
self.logger.log(f"첫 번째 항목 발견: {current_option}", level=logging.DEBUG)
|
||||
|
||||
# ↓ 누르기
|
||||
# 이제 ArrowDown을 눌러서 다음 항목으로 이동
|
||||
for _ in range(200): # 최대 200개 그룹 가정
|
||||
await self.page.keyboard.press("ArrowDown")
|
||||
await asyncio.sleep(0.05)
|
||||
await asyncio.sleep(0.1) # 키 입력 후 대기 시간 증가
|
||||
|
||||
# 마지막 도달 확인 (더 이상 이동 불가)
|
||||
option_active = await self.page.evaluate("""
|
||||
current_option = await self.page.evaluate("""
|
||||
() => {
|
||||
const active = document.querySelector('.ant-select-item-option-active');
|
||||
return active ? active.getAttribute("title") : null;
|
||||
const active = document.querySelector('.ant-select-item-option-active .ant-select-item-option-content');
|
||||
return active ? active.textContent.trim() : null;
|
||||
}
|
||||
""")
|
||||
if option_active and option_active in seen and len(seen) == len(group_names):
|
||||
|
||||
if not current_option:
|
||||
# 활성 항목을 찾을 수 없으면 종료
|
||||
break
|
||||
|
||||
if current_option not in seen:
|
||||
# 새로운 항목 발견
|
||||
seen.add(current_option)
|
||||
group_names.append(current_option)
|
||||
consecutive_repeats = 0 # 연속 반복 카운터 리셋
|
||||
self.logger.log(f"발견: {current_option}", level=logging.DEBUG)
|
||||
else:
|
||||
# 이미 본 항목을 다시 만남 (순환 시작)
|
||||
consecutive_repeats += 1
|
||||
# 첫 번째 항목을 다시 만났고, 이미 모든 항목을 수집했다면 종료
|
||||
if current_option == first_item and len(group_names) > 0:
|
||||
self.logger.log(f"첫 번째 항목으로 돌아옴. 수집 완료: {len(group_names)}개", level=logging.DEBUG)
|
||||
break
|
||||
# 연속으로 같은 항목을 여러 번 만나면 종료 (안전장치)
|
||||
if consecutive_repeats >= 3:
|
||||
self.logger.log(f"연속 반복 감지. 수집 완료: {len(group_names)}개", level=logging.DEBUG)
|
||||
break
|
||||
|
||||
await self.page.keyboard.press("Escape")
|
||||
|
|
@ -6768,6 +6810,19 @@ class BrowserController(QThread):
|
|||
else:
|
||||
self.logger.log("start_browser_task - 실행 중인 이벤트 루프가 없습니다.", level=logging.ERROR)
|
||||
|
||||
|
||||
def get_group_names_list_all_task(self):
|
||||
"""이벤트 루프에서 작업그룹 목록 가져오기 작업 추가"""
|
||||
self.logger.log(f"get_group_names_list_all_async - 작업그룹 이름 목록 가져오기 시작 : {self.toggle_states}", level=logging.DEBUG)
|
||||
|
||||
# 실행 중인 이벤트 루프에 비동기 작업 추가
|
||||
if self.loop and not self.loop.is_closed():
|
||||
asyncio.run_coroutine_threadsafe(self.get_group_names_list_all_async(), self.loop)
|
||||
self.browser_task_started = True # 작업 실행 플래그 설정
|
||||
self.logger.log("get_group_names_list_all_async - 비동기 작업이 추가되었습니다.", level=logging.DEBUG)
|
||||
else:
|
||||
self.logger.log("get_group_names_list_all_async - 실행 중인 이벤트 루프가 없습니다.", level=logging.ERROR)
|
||||
|
||||
def select_group_task_list(self, group_index: int):
|
||||
"""이벤트 루프에서 그룹 선택 작업 실행"""
|
||||
self.logger.log(f"select_group_task - 그룹 선택 작업 추가: {group_index}", level=logging.DEBUG)
|
||||
|
|
|
|||
603
build_nuitka.py
603
build_nuitka.py
|
|
@ -13,6 +13,12 @@ cx_Freeze 대비 강력한 코드 보호 (리버스 엔지니어링 어려움)
|
|||
- Nuitka가 자동으로 MinGW64 다운로드 제안함
|
||||
"""
|
||||
|
||||
# 인코딩 설정 (Windows PowerShell 호환성)
|
||||
import sys
|
||||
import io
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
|
@ -55,13 +61,20 @@ def get_nuitka_command():
|
|||
|
||||
# ===== 플러그인 =====
|
||||
"--enable-plugin=pyside6", # PySide6 지원
|
||||
"--enable-plugin=multiprocessing", # multiprocessing 지원
|
||||
# multiprocessing 플러그인은 Nuitka에서 자동으로 활성화되므로 명시할 필요 없음
|
||||
|
||||
# ===== 포함할 패키지 =====
|
||||
"--include-package=PySide6",
|
||||
"--include-package=shiboken6",
|
||||
"--include-package=PIL",
|
||||
"--include-package=PIL.Image",
|
||||
"--include-package=PIL.ImageOps",
|
||||
"--include-package=PIL.ImageEnhance",
|
||||
"--include-package=PIL.ImageFilter",
|
||||
"--include-package=PIL.features",
|
||||
"--include-package=numpy",
|
||||
"--include-package=numpy.core",
|
||||
"--include-package=numpy.random",
|
||||
"--include-package=pandas",
|
||||
"--include-package=requests",
|
||||
"--include-package=bs4",
|
||||
|
|
@ -74,6 +87,10 @@ def get_nuitka_command():
|
|||
"--include-package=pydantic",
|
||||
"--include-package=pydantic_core",
|
||||
"--include-package=httpx",
|
||||
"--include-package=httpx.__version__",
|
||||
"--include-package=httpx._models",
|
||||
"--include-package=httpx._client",
|
||||
"--include-package=httpx._config",
|
||||
"--include-package=translatepy",
|
||||
"--include-package=markdown",
|
||||
"--include-package=psutil",
|
||||
|
|
@ -83,6 +100,11 @@ def get_nuitka_command():
|
|||
"--include-package=win32com",
|
||||
"--include-package=pythoncom",
|
||||
"--include-package=curl_cffi",
|
||||
"--include-package=json",
|
||||
"--include-package=json.encoder",
|
||||
"--include-package=json.decoder",
|
||||
"--include-package=json.scanner",
|
||||
"--include-package=pathlib",
|
||||
|
||||
# ===== 포함할 모듈 (프로젝트 내부) =====
|
||||
"--include-module=loggerModule",
|
||||
|
|
@ -94,22 +116,45 @@ def get_nuitka_command():
|
|||
"--include-module=mainUI_SP",
|
||||
"--include-module=limited_gui",
|
||||
|
||||
# src 하위 모듈들
|
||||
# src 하위 모듈들 (자세한 모듈 지정으로 누락 방지)
|
||||
"--include-package=src",
|
||||
"--include-package=src.cmdDiag",
|
||||
"--include-package=src.inputDiag",
|
||||
"--include-package=src.keyword",
|
||||
"--include-package=src.priceSetDiag",
|
||||
"--include-package=src.contents",
|
||||
"--include-package=src.contents.option",
|
||||
"--include-package=src.contents.price",
|
||||
"--include-package=src.contents.details",
|
||||
"--include-package=src.contents.titleGenerator",
|
||||
"--include-package=src.contents.thumb",
|
||||
"--include-package=src.contents.tags",
|
||||
"--include-package=src.titleManager",
|
||||
"--include-package=src.titleManager.sp_ForbiddenM",
|
||||
"--include-package=src.titleManager.gpt_client",
|
||||
"--include-package=src.translator",
|
||||
"--include-package=src.translator.papago_translator",
|
||||
"--include-package=src.discord_manager",
|
||||
"--include-package=src.unwantedDiag",
|
||||
"--include-package=src.unwantedDiag.unwanted_words_dialog",
|
||||
"--include-package=src.logDialog",
|
||||
"--include-package=src.logDialog.log_dialog",
|
||||
"--include-package=src.logDialog.log_filter",
|
||||
"--include-package=src.modules",
|
||||
"--include-package=src.modules.settings_manager",
|
||||
"--include-package=src.modules.gpu_status_checker",
|
||||
"--include-package=src.modules.gpu_utils",
|
||||
"--include-package=src.modules.image_worker_client",
|
||||
"--include-package=src.modules.fonts.fontSelectDialog",
|
||||
"--include-package=src.gpuDiag",
|
||||
"--include-package=src.img_module",
|
||||
"--include-package=src.img_module.image_processor_manager",
|
||||
"--include-package=src.img_module.image_processor_dialog",
|
||||
"--include-package=src.lens",
|
||||
"--include-package=src.lens.naver_lens_client",
|
||||
"--include-package=src.lens.naver_lens_parser",
|
||||
"--include-package=src.lens.naver_lens_adapter",
|
||||
"--include-package=src.lens.aliprice_lens_client",
|
||||
"--include-package=src.ui",
|
||||
"--include-package=src.loggerModules",
|
||||
"--include-package=updateManager",
|
||||
|
|
@ -118,15 +163,32 @@ def get_nuitka_command():
|
|||
"--nofollow-import-to=tkinter",
|
||||
"--nofollow-import-to=PyQt4",
|
||||
"--nofollow-import-to=PyQt5",
|
||||
"--nofollow-import-to=AppKit",
|
||||
"--nofollow-import-to=Foundation",
|
||||
"--nofollow-import-to=matplotlib",
|
||||
"--nofollow-import-to=IPython",
|
||||
"--nofollow-import-to=OpenSSL",
|
||||
"--nofollow-import-to=curses",
|
||||
"--nofollow-import-to=test",
|
||||
"--nofollow-import-to=asyncpg",
|
||||
"--nofollow-import-to=pytest",
|
||||
"--nofollow-import-to=hypothesis",
|
||||
"--nofollow-import-to=mypy",
|
||||
"--nofollow-import-to=coverage",
|
||||
"--nofollow-import-to=tox",
|
||||
"--nofollow-import-to=sympy",
|
||||
"--nofollow-import-to=sympy.core",
|
||||
"--nofollow-import-to=sympy.ntheory",
|
||||
"--nofollow-import-to=sympy.external",
|
||||
"--nofollow-import-to=sympy.polys",
|
||||
"--nofollow-import-to=sympy.*",
|
||||
"--nofollow-import-to=mpmath",
|
||||
"--nofollow-import-to=gmpy2",
|
||||
"--nofollow-import-to=scipy",
|
||||
"--nofollow-import-to=scipy.special",
|
||||
"--nofollow-import-to=scipy.sparse",
|
||||
"--nofollow-import-to=scipy.linalg",
|
||||
"--nofollow-import-to=scipy.ndimage",
|
||||
"--nofollow-import-to=torch",
|
||||
"--nofollow-import-to=tensorflow",
|
||||
"--nofollow-import-to=keras",
|
||||
|
|
@ -137,6 +199,25 @@ def get_nuitka_command():
|
|||
"--nofollow-import-to=onnx",
|
||||
"--nofollow-import-to=skimage",
|
||||
"--nofollow-import-to=scikit-image",
|
||||
"--nofollow-import-to=pyclipper",
|
||||
"--nofollow-import-to=shapely",
|
||||
"--nofollow-import-to=imgaug",
|
||||
"--nofollow-import-to=albumentations",
|
||||
# ImageProcessor3 관련 모듈 제외
|
||||
"--nofollow-import-to=src.modules.image_processor3",
|
||||
"--nofollow-import-to=src.modules.image_processor4",
|
||||
"--nofollow-import-to=src.modules.image_worker",
|
||||
"--nofollow-import-to=src.modules.image_worker_manager",
|
||||
"--nofollow-import-to=src.modules.ocr_module",
|
||||
"--nofollow-import-to=src.modules.mask_module_for_paddle",
|
||||
"--nofollow-import-to=src.modules.text_rendering_module",
|
||||
"--nofollow-import-to=src.modules.postImageManager",
|
||||
"--nofollow-import-to=src.modules.request_inpaint",
|
||||
"--nofollow-import-to=src.modules.migan_module",
|
||||
"--nofollow-import-to=src.modules.background_removal_module",
|
||||
"--nofollow-import-to=src.modules.bria_background_removal_module",
|
||||
"--nofollow-import-to=src.modules.gemma_client",
|
||||
"--nofollow-import-to=src.modules.onnx_ocr_module",
|
||||
|
||||
# ===== 최적화 옵션 =====
|
||||
"--remove-output", # 이전 빌드 결과 삭제
|
||||
|
|
@ -157,8 +238,9 @@ def copy_data_files():
|
|||
|
||||
# 복사할 파일/디렉토리 목록 (소스, 대상)
|
||||
data_files = [
|
||||
# 아이콘
|
||||
# 메인 아이콘 (실행 파일 아이콘)
|
||||
("Edit_PartTimer3.ico", "Edit_PartTimer3.ico"),
|
||||
# 내부 아이콘
|
||||
("src/Edit_PartTimer3.ico", "lib/src/Edit_PartTimer3.ico"),
|
||||
|
||||
# JSON 설정 파일
|
||||
|
|
@ -201,11 +283,11 @@ def copy_data_files():
|
|||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
try:
|
||||
shutil.copy2(src_path, dest_path)
|
||||
print(f" ✓ 복사: {src} -> {dest}")
|
||||
print(f" [OK] 복사: {src} -> {dest}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 실패: {src} - {e}")
|
||||
print(f" [ERROR] 실패: {src} - {e}")
|
||||
else:
|
||||
print(f" ⚠ 없음: {src}")
|
||||
print(f" [WARN] 없음: {src}")
|
||||
|
||||
# 디렉토리 복사
|
||||
for src, dest in data_dirs:
|
||||
|
|
@ -218,15 +300,35 @@ def copy_data_files():
|
|||
if os.path.exists(dest_path):
|
||||
shutil.rmtree(dest_path)
|
||||
shutil.copytree(src_path, dest_path)
|
||||
print(f" ✓ 복사 (디렉토리): {src} -> {dest}")
|
||||
print(f" [OK] 복사 (디렉토리): {src} -> {dest}")
|
||||
except Exception as e:
|
||||
print(f" ✗ 실패: {src} - {e}")
|
||||
print(f" [ERROR] 실패: {src} - {e}")
|
||||
else:
|
||||
print(f" ⚠ 없음: {src}")
|
||||
print(f" [WARN] 없음: {src}")
|
||||
|
||||
print("데이터 파일 복사 완료!\n")
|
||||
|
||||
|
||||
def copy_exe_to_dist():
|
||||
"""Nuitka가 생성한 실행 파일을 최종 dist 폴더로 복사"""
|
||||
main_dist_exe = os.path.join(OUTPUT_DIR, "main.dist", f"{__exe_name__}.exe")
|
||||
final_exe_path = os.path.join(DIST_DIR, f"{__exe_name__}.exe")
|
||||
|
||||
if os.path.exists(main_dist_exe):
|
||||
try:
|
||||
# 대상 디렉토리가 없으면 생성
|
||||
os.makedirs(os.path.dirname(final_exe_path), exist_ok=True)
|
||||
shutil.copy2(main_dist_exe, final_exe_path)
|
||||
print(f" [OK] 실행 파일 복사: {main_dist_exe} -> {final_exe_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" [ERROR] 실행 파일 복사 실패: {e}")
|
||||
return False
|
||||
else:
|
||||
print(f" [WARN] 실행 파일을 찾을 수 없습니다: {main_dist_exe}")
|
||||
return False
|
||||
|
||||
|
||||
def cleanup_unnecessary_files():
|
||||
"""불필요한 파일 정리 (ImageProcessor3 관련 등)"""
|
||||
|
||||
|
|
@ -266,15 +368,15 @@ def cleanup_unnecessary_files():
|
|||
)
|
||||
total_size_saved += dir_size
|
||||
shutil.rmtree(target_path)
|
||||
print(f" ✓ 삭제: {item} ({dir_size / (1024*1024):.1f} MB)")
|
||||
print(f" [OK] 삭제: {item} ({dir_size / (1024*1024):.1f} MB)")
|
||||
else:
|
||||
file_size = os.path.getsize(target_path)
|
||||
total_size_saved += file_size
|
||||
os.remove(target_path)
|
||||
print(f" ✓ 삭제: {item} ({file_size / (1024*1024):.2f} MB)")
|
||||
print(f" [OK] 삭제: {item} ({file_size / (1024*1024):.2f} MB)")
|
||||
removed_count += 1
|
||||
except Exception as e:
|
||||
print(f" ✗ 삭제 실패: {item} - {e}")
|
||||
print(f" [ERROR] 삭제 실패: {item} - {e}")
|
||||
|
||||
if removed_count > 0:
|
||||
print(f"\n정리 완료: {removed_count}개 항목 삭제, 약 {total_size_saved / (1024*1024):.1f} MB 절약")
|
||||
|
|
@ -282,32 +384,465 @@ def cleanup_unnecessary_files():
|
|||
print("정리할 항목이 없습니다.")
|
||||
|
||||
|
||||
def run_inno_setup():
|
||||
"""Inno Setup 스크립트 생성"""
|
||||
def copy_exe_to_dist():
|
||||
"""Nuitka가 생성한 실행 파일을 최종 dist 폴더로 복사"""
|
||||
main_dist_exe = os.path.join(OUTPUT_DIR, "main.dist", f"{__exe_name__}.exe")
|
||||
final_exe_path = os.path.join(DIST_DIR, f"{__exe_name__}.exe")
|
||||
|
||||
if os.path.exists(main_dist_exe):
|
||||
try:
|
||||
result = subprocess.run([sys.executable, "generate_iss.py"], capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print(result.stdout)
|
||||
print("\nInno Setup 스크립트 생성 완료!")
|
||||
else:
|
||||
print(f"Inno Setup 스크립트 생성 중 오류: {result.stderr}")
|
||||
shutil.copy2(main_dist_exe, final_exe_path)
|
||||
print(f" [OK] 실행 파일 복사: {main_dist_exe} -> {final_exe_path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Inno Setup 스크립트 생성 중 예외: {e}")
|
||||
print(f" [ERROR] 실행 파일 복사 실패: {e}")
|
||||
return False
|
||||
else:
|
||||
print(f" [WARN] 실행 파일을 찾을 수 없습니다: {main_dist_exe}")
|
||||
return False
|
||||
|
||||
|
||||
def generate_inno_setup_script():
|
||||
"""Nuitka 빌드 결과물을 위한 Inno Setup 스크립트 생성"""
|
||||
try:
|
||||
import datetime
|
||||
from updateManager.__version__ import (
|
||||
__program_id__, __program_name__, __company_name__,
|
||||
__exe_name__, __setup_name__, __icon_file__, __setup_output_dir__,
|
||||
__version__, __description__, __copyright__
|
||||
)
|
||||
|
||||
# 현재 날짜와 시간
|
||||
now = datetime.datetime.now()
|
||||
date_str = now.strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
# 생성할 .iss 파일 이름
|
||||
iss_filename = f"AutoPercenty_Nuitka_{date_str}.iss"
|
||||
|
||||
# Nuitka 빌드 결과물 경로 (cx_Freeze와 다른 경로)
|
||||
# 상대 경로로 변환 (프로젝트 루트 기준) - Inno Setup은 프로젝트 루트에서 실행됨
|
||||
nuitka_build_path_rel = os.path.relpath(DIST_DIR, BASE_DIR).replace("\\", "\\\\")
|
||||
|
||||
# 템플릿 작성 (cx_Freeze 버전과 동일하지만 소스 경로만 변경)
|
||||
iss_template = fr"""; AutoPercenty3 Inno Setup Script (Nuitka Build)
|
||||
; 이 스크립트는 Nuitka로 빌드된 결과물이 있는 "{DIST_DIR}" 폴더를 기반으로 인스톨러를 제작합니다.
|
||||
; {date_str}에 생성됨
|
||||
|
||||
#define AppId "{__program_id__}"
|
||||
#define MyAppName "{__title__}"
|
||||
#define MyAppVersion "{__version__}"
|
||||
#define MyAppPublisher "{__company_name__}"
|
||||
#define MyAppProgramName "{__program_name__}"
|
||||
#define MyAppDescription "{__description__}"
|
||||
#define MyAppCopyright "{__copyright__}"
|
||||
#define MyAppExeName "{__exe_name__}"
|
||||
#define MySetupName "{__setup_name__}"
|
||||
#define MySetupIcon "{__icon_file__}"
|
||||
#define MySetupOutputDir "{__setup_output_dir__}"
|
||||
#define NuitkaBuildPath "{nuitka_build_path_rel}"
|
||||
|
||||
[Setup]
|
||||
; 기본 설정
|
||||
AppId={{#AppId}}
|
||||
AppName={{#MyAppProgramName}}
|
||||
AppVersion={{#MyAppVersion}}
|
||||
AppPublisher={{#MyAppPublisher}}
|
||||
DefaultDirName={{autopf}}\{{#MyAppName}}
|
||||
DefaultGroupName={{#MyAppPublisher}}
|
||||
OutputDir={{#MySetupOutputDir}}
|
||||
OutputBaseFilename={{#MySetupName}}
|
||||
SetupIconFile={{#MySetupIcon}}
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
|
||||
; 업데이트 관련 설정 - 권한 최적화
|
||||
PrivilegesRequired=admin
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
UpdateUninstallLogAppName=yes
|
||||
AppMutex={{#MyAppName}}
|
||||
CloseApplications=yes
|
||||
RestartApplications=no
|
||||
|
||||
; 보안 및 호환성 설정
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
AllowNoIcons=yes
|
||||
|
||||
; 버전 정보
|
||||
VersionInfoVersion={{#MyAppVersion}}
|
||||
VersionInfoCompany={{#MyAppPublisher}}
|
||||
VersionInfoDescription={{#MyAppDescription}}
|
||||
VersionInfoCopyright={{#MyAppCopyright}}
|
||||
VersionInfoProductName={{#MyAppProgramName}}
|
||||
VersionInfoProductVersion={{#MyAppVersion}}
|
||||
|
||||
[Languages]
|
||||
Name: "korean"; MessagesFile: "compiler:Languages\Korean.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopicon"; Description: "{{cm:CreateDesktopIcon}}"; GroupDescription: "{{cm:AdditionalIcons}}"
|
||||
|
||||
[Dirs]
|
||||
; 설치 시 {{app}}\logs 폴더를 생성하고,
|
||||
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
|
||||
Name: "{{app}}\logs"; Permissions: users-modify
|
||||
; 설치 시 {{app}}\user_data 폴더를 생성하고,
|
||||
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
|
||||
Name: "{{app}}\user_data"; Permissions: users-modify
|
||||
; Playwright 브라우저 폴더를 Program Files 내부에 생성
|
||||
Name: "{{app}}\lib\src\browsers\chromium-1200"; Permissions: users-modify
|
||||
; Playwright 브라우저 사용자폴더를 Program Files 내부에 생성
|
||||
Name: "{{app}}\lib\src\browsers\user_data"; Permissions: users-modify
|
||||
|
||||
[Files]
|
||||
; Nuitka 빌드 결과물 전체 설치 (항상 덮어쓰기)
|
||||
Source: "{{#NuitkaBuildPath}}\*"; DestDir: "{{app}}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
; VC++ 재배포 패키지 파일을 임시 폴더({{tmp}})에 복사
|
||||
Source: "VC_redist.x64.exe"; DestDir: "{{tmp}}"; Flags: deleteafterinstall
|
||||
|
||||
[Registry]
|
||||
; Playwright 브라우저 경로를 Program Files 내부로 설정
|
||||
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "PLAYWRIGHT_BROWSERS_PATH"; ValueData: "{{app}}\lib\src\browsers"; Flags: preservestringtype
|
||||
|
||||
[Icons]
|
||||
; 시작 메뉴 바로가기
|
||||
Name: "{{group}}\{{#MyAppProgramName}}"; Filename: "{{app}}\{{#MyAppExeName}}.exe"
|
||||
; 바탕화면 바로가기
|
||||
Name: "{{autodesktop}}\{{#MyAppProgramName}}"; Filename: "{{app}}\{{#MyAppExeName}}.exe"; Tasks: desktopicon
|
||||
; 프로그램 제거 바로가기
|
||||
Name: "{{group}}\{{#MyAppProgramName}} 제거"; Filename: "{{uninstallexe}}"
|
||||
|
||||
[Run]
|
||||
; VC++ 재배포 패키지 설치 (필요할 경우)
|
||||
Filename: "{{tmp}}\VC_redist.x64.exe"; Parameters: "/install /passive /norestart"; StatusMsg: "VC++ 재배포 패키지 설치 중..."; Check: NeedsVCredist
|
||||
; 설치 후 프로그램 실행 (원할 경우)
|
||||
Filename: "{{app}}\{{#MyAppExeName}}.exe"; Description: "{{cm:LaunchProgram,{{#MyAppProgramName}}}}"; Flags: nowait postinstall skipifsilent
|
||||
|
||||
[Code]
|
||||
function CompareVersion(V1, V2: string): Integer;
|
||||
var
|
||||
P1, P2, N1, N2: Integer;
|
||||
begin
|
||||
P1 := 1;
|
||||
P2 := 1;
|
||||
Result := 0;
|
||||
while (Result = 0) and ((P1 <= Length(V1)) or (P2 <= Length(V2))) do begin
|
||||
while (P1 <= Length(V1)) and (V1[P1] = '.') do Inc(P1);
|
||||
while (P2 <= Length(V2)) and (V2[P2] = '.') do Inc(P2);
|
||||
if (P1 <= Length(V1)) and (P2 <= Length(V2)) then begin
|
||||
N1 := 0; while (P1 <= Length(V1)) and (V1[P1] >= '0') and (V1[P1] <= '9') do begin N1 := N1 * 10 + Ord(V1[P1]) - Ord('0'); Inc(P1); end;
|
||||
N2 := 0; while (P2 <= Length(V2)) and (V2[P2] >= '0') and (V2[P2] <= '9') do begin N2 := N2 * 10 + Ord(V2[P2]) - Ord('0'); Inc(P2); end;
|
||||
if N1 < N2 then Result := -1 else if N1 > N2 then Result := 1;
|
||||
end else begin
|
||||
if P1 <= Length(V1) then Result := 1 else if P2 <= Length(V2) then Result := -1;
|
||||
end;
|
||||
while (P1 <= Length(V1)) and (V1[P1] <> '.') do Inc(P1);
|
||||
while (P2 <= Length(V2)) and (V2[P2] <> '.') do Inc(P2);
|
||||
end;
|
||||
end;
|
||||
|
||||
// 파일 또는 폴더 복사 함수
|
||||
procedure CopyDir(const SourcePath, DestPath: string);
|
||||
var
|
||||
FindRec: TFindRec;
|
||||
SourceFilePath: string;
|
||||
DestFilePath: string;
|
||||
begin
|
||||
ForceDirectories(DestPath);
|
||||
|
||||
if FindFirst(SourcePath + '\*', FindRec) then
|
||||
begin
|
||||
try
|
||||
repeat
|
||||
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
|
||||
begin
|
||||
SourceFilePath := SourcePath + '\' + FindRec.Name;
|
||||
DestFilePath := DestPath + '\' + FindRec.Name;
|
||||
|
||||
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
|
||||
begin
|
||||
if FileCopy(SourceFilePath, DestFilePath, False) then
|
||||
Log('파일 복사 성공: ' + SourceFilePath + ' -> ' + DestFilePath)
|
||||
else
|
||||
Log('파일 복사 실패: ' + SourceFilePath);
|
||||
end
|
||||
else
|
||||
CopyDir(SourceFilePath, DestFilePath);
|
||||
end;
|
||||
until not FindNext(FindRec);
|
||||
finally
|
||||
FindClose(FindRec);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
// 디렉토리 삭제 함수
|
||||
procedure DeleteDir(const DirPath: string);
|
||||
var
|
||||
FindRec: TFindRec;
|
||||
FilePath: string;
|
||||
begin
|
||||
if not DirExists(DirPath) then Exit;
|
||||
|
||||
if FindFirst(DirPath + '\*', FindRec) then
|
||||
begin
|
||||
try
|
||||
repeat
|
||||
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
|
||||
begin
|
||||
FilePath := DirPath + '\' + FindRec.Name;
|
||||
|
||||
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
|
||||
begin
|
||||
if DeleteFile(FilePath) then
|
||||
Log('파일 삭제 성공: ' + FilePath)
|
||||
else
|
||||
Log('파일 삭제 실패: ' + FilePath);
|
||||
end
|
||||
else
|
||||
DeleteDir(FilePath);
|
||||
end;
|
||||
until not FindNext(FindRec);
|
||||
finally
|
||||
FindClose(FindRec);
|
||||
end;
|
||||
end;
|
||||
|
||||
if RemoveDir(DirPath) then
|
||||
Log('디렉토리 삭제 성공: ' + DirPath)
|
||||
else
|
||||
Log('디렉토리 삭제 실패: ' + DirPath);
|
||||
end;
|
||||
|
||||
// 프로그램 실행 여부 확인
|
||||
function IsAppRunning(const FileName: string): Boolean;
|
||||
var
|
||||
Handle: THandle;
|
||||
begin
|
||||
Handle := FindWindowByWindowName('{{#MyAppProgramName}}');
|
||||
Result := (Handle <> 0);
|
||||
end;
|
||||
|
||||
// 프로그램 종료
|
||||
procedure CloseApplication(const FileName: string);
|
||||
var
|
||||
Handle: THandle;
|
||||
begin
|
||||
Handle := FindWindowByWindowName('{{#MyAppProgramName}}');
|
||||
if Handle <> 0 then
|
||||
begin
|
||||
PostMessage(Handle, 18, 0, 0);
|
||||
Sleep(1000);
|
||||
end;
|
||||
end;
|
||||
|
||||
// VC++ 재배포 패키지 필요 여부 확인
|
||||
function NeedsVCredist: Boolean;
|
||||
begin
|
||||
if RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64') then
|
||||
Result := False
|
||||
else
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
// 설치 완료 후 실행 여부 확인
|
||||
function InitializeFinish(): Boolean;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := True;
|
||||
if MsgBox('설치가 완료되었습니다. 프로그램을 실행하시겠습니까?' + #13#10 +
|
||||
'(실행 시 서버와 동기화하여 설정이 업데이트됩니다)',
|
||||
mbConfirmation, MB_YESNO) = IDYES then
|
||||
begin
|
||||
Exec(ExpandConstant('{{app}}\{{#MyAppExeName}}.exe'), '', '', SW_SHOW, ewNoWait, ResultCode);
|
||||
end;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
var
|
||||
OldVersion: String;
|
||||
NewVersion: String;
|
||||
OldAppPath: String;
|
||||
UserDataSourcePath, UserDataBackupPath: String;
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Result := True;
|
||||
NewVersion := '{{#MyAppVersion}}';
|
||||
UserDataBackupPath := ExpandConstant('{{tmp}}\user_data_backup');
|
||||
|
||||
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{#MyAppName}}_is1',
|
||||
'DisplayVersion', OldVersion) then
|
||||
begin
|
||||
if CompareVersion(OldVersion, NewVersion) >= 0 then
|
||||
begin
|
||||
MsgBox('현재 설치된 버전(' + OldVersion + ')이 이 설치 프로그램의 버전(' +
|
||||
NewVersion + ')과 같거나 더 높습니다.' + #13#10 +
|
||||
'설치를 계속할 수 없습니다.', mbInformation, MB_OK);
|
||||
Result := False;
|
||||
exit;
|
||||
end;
|
||||
|
||||
if CompareVersion(OldVersion, NewVersion) < 0 then
|
||||
begin
|
||||
Log('업데이트 설치 진행: ' + OldVersion + ' -> ' + NewVersion);
|
||||
|
||||
if IsAppRunning('{{#MyAppExeName}}.exe') then
|
||||
begin
|
||||
if MsgBox('프로그램을 업데이트하기 위해 실행 중인 프로그램을 종료해야 합니다.' + #13#10 +
|
||||
'계속하시겠습니까?', mbConfirmation, MB_YESNO) = IDNO then
|
||||
begin
|
||||
Result := False;
|
||||
exit;
|
||||
end;
|
||||
CloseApplication('{{#MyAppExeName}}.exe');
|
||||
Sleep(2000);
|
||||
|
||||
Log('프로세스 강제 종료 시도: {{#MyAppExeName}}.exe');
|
||||
Exec('taskkill', '/f /im {{#MyAppExeName}}.exe /t', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
Sleep(3000);
|
||||
end;
|
||||
|
||||
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{{#MyAppName}}_is1',
|
||||
'InstallLocation', OldAppPath) then
|
||||
begin
|
||||
Log('기존 설치 경로: ' + OldAppPath);
|
||||
|
||||
UserDataSourcePath := OldAppPath + '\lib\src\user_data';
|
||||
if DirExists(UserDataSourcePath) then
|
||||
begin
|
||||
Log('사용자 데이터 백업 중: ' + UserDataSourcePath + ' -> ' + UserDataBackupPath);
|
||||
ForceDirectories(UserDataBackupPath);
|
||||
CopyDir(UserDataSourcePath, UserDataBackupPath);
|
||||
end
|
||||
else
|
||||
begin
|
||||
Log('사용자 데이터 폴더가 존재하지 않음: ' + UserDataSourcePath);
|
||||
end;
|
||||
|
||||
if DirExists(OldAppPath) then
|
||||
begin
|
||||
Log('기존 설치 폴더 삭제 중: ' + OldAppPath);
|
||||
DeleteDir(OldAppPath);
|
||||
Sleep(2000);
|
||||
if DirExists(OldAppPath) then
|
||||
begin
|
||||
Log('설치 폴더 삭제 재시도 (Windows 명령어 사용): ' + OldAppPath);
|
||||
Exec('cmd.exe', '/c rmdir /s /q "' + OldAppPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
Sleep(2000);
|
||||
end;
|
||||
|
||||
if DirExists(OldAppPath) then
|
||||
begin
|
||||
Log('설치 폴더 삭제 3차 시도 (PowerShell 사용): ' + OldAppPath);
|
||||
Exec('powershell.exe', '-Command "Remove-Item -Path ''' + OldAppPath + ''' -Recurse -Force -ErrorAction SilentlyContinue"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||||
Sleep(2000);
|
||||
end;
|
||||
|
||||
if DirExists(OldAppPath) then
|
||||
begin
|
||||
Log('경고: 설치 폴더 삭제 실패: ' + OldAppPath);
|
||||
if MsgBox('기존 설치 폴더를 완전히 삭제하지 못했습니다.' + #13#10 +
|
||||
'이전 버전의 파일들이 남아있어 새 버전과 충돌할 수 있습니다.' + #13#10 + #13#10 +
|
||||
'폴더: ' + OldAppPath + #13#10 + #13#10 +
|
||||
'설치를 계속하시겠습니까?',
|
||||
mbConfirmation, MB_YESNO) = IDNO then
|
||||
begin
|
||||
Result := False;
|
||||
exit;
|
||||
end;
|
||||
end
|
||||
else
|
||||
begin
|
||||
Log('기존 설치 폴더 삭제 완료: ' + OldAppPath);
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure CurStepChanged(CurStep: TSetupStep);
|
||||
var
|
||||
UserDataBackupPath, UserDataDestPath: String;
|
||||
begin
|
||||
if CurStep = ssPostInstall then
|
||||
begin
|
||||
UserDataBackupPath := ExpandConstant('{{tmp}}\user_data_backup');
|
||||
UserDataDestPath := ExpandConstant('{{app}}\lib\src\user_data');
|
||||
|
||||
if DirExists(UserDataBackupPath) then
|
||||
begin
|
||||
Log('사용자 데이터 복원 중: ' + UserDataBackupPath + ' -> ' + UserDataDestPath);
|
||||
ForceDirectories(UserDataDestPath);
|
||||
CopyDir(UserDataBackupPath, UserDataDestPath);
|
||||
Log('사용자 데이터 복원 완료');
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
"""
|
||||
|
||||
# .iss 파일 저장
|
||||
with open(iss_filename, 'w', encoding='utf-8') as f:
|
||||
f.write(iss_template)
|
||||
|
||||
print(f"\n[OK] Inno Setup 스크립트 생성 완료: {iss_filename}")
|
||||
|
||||
# 매니페스트 파일도 생성
|
||||
manifest = {
|
||||
"app_id": __program_id__,
|
||||
"app_name": __program_name__,
|
||||
"publisher": __company_name__,
|
||||
"appexe": f"{__exe_name__}.exe",
|
||||
"default_dirname": __title__,
|
||||
"version": __version__
|
||||
}
|
||||
|
||||
out_dir = __setup_output_dir__
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
manifest_path = os.path.join(out_dir, f"{__setup_name__}.manifest.json")
|
||||
|
||||
import json
|
||||
with open(manifest_path, "w", encoding="utf-8") as mf:
|
||||
json.dump(manifest, mf, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f"[OK] 매니페스트 생성: {manifest_path}")
|
||||
|
||||
return iss_filename
|
||||
|
||||
except Exception as e:
|
||||
print(f"[ERROR] Inno Setup 스크립트 생성 중 오류: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print(f" Nuitka 빌드 시작 - {__title__} v{__version__}")
|
||||
print("=" * 60)
|
||||
print("\n⚠️ 첫 빌드 시 C 컴파일러(MinGW64) 다운로드가 필요할 수 있습니다.")
|
||||
print("⚠️ 빌드 시간: 약 10-30분 소요 (PC 사양에 따라 다름)\n")
|
||||
print("\n[주의] 첫 빌드 시 C 컴파일러(MinGW64) 다운로드가 필요할 수 있습니다.")
|
||||
print("[주의] 빌드 시간: 약 10-30분 소요 (PC 사양에 따라 다름)\n")
|
||||
|
||||
# Nuitka 설치 확인
|
||||
try:
|
||||
import nuitka
|
||||
print(f"✓ Nuitka 버전: {nuitka.__version__}")
|
||||
# Nuitka 버전 확인 (다양한 방법 시도)
|
||||
try:
|
||||
version = nuitka.__version__
|
||||
except AttributeError:
|
||||
try:
|
||||
import nuitka.Version
|
||||
version = nuitka.Version.getNuitkaVersion()
|
||||
except (AttributeError, ImportError):
|
||||
try:
|
||||
import pkg_resources
|
||||
version = pkg_resources.get_distribution('nuitka').version
|
||||
except:
|
||||
version = "알 수 없음"
|
||||
print(f"[OK] Nuitka 버전: {version}")
|
||||
except ImportError:
|
||||
print("✗ Nuitka가 설치되어 있지 않습니다.")
|
||||
print("[ERROR] Nuitka가 설치되어 있지 않습니다.")
|
||||
print(" 설치 명령어: pip install nuitka ordered-set zstandard")
|
||||
sys.exit(1)
|
||||
|
||||
|
|
@ -328,16 +863,30 @@ def main():
|
|||
# 데이터 파일 복사
|
||||
copy_data_files()
|
||||
|
||||
# 실행 파일을 최종 dist 폴더로 복사
|
||||
print("\n실행 파일 복사 중...")
|
||||
if copy_exe_to_dist():
|
||||
print("실행 파일 복사 완료!\n")
|
||||
else:
|
||||
print("실행 파일 복사 실패!\n")
|
||||
|
||||
# 불필요한 파일 정리
|
||||
cleanup_unnecessary_files()
|
||||
|
||||
# Inno Setup 스크립트 생성 (선택적)
|
||||
# run_inno_setup()
|
||||
# Inno Setup 스크립트 생성
|
||||
print("\nInno Setup 스크립트 생성 중...")
|
||||
iss_file = generate_inno_setup_script()
|
||||
if iss_file:
|
||||
print(f"\n[OK] Inno Setup 스크립트 생성 완료: {iss_file}")
|
||||
else:
|
||||
print("\n[WARN] Inno Setup 스크립트 생성 실패")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f" 빌드 완료!")
|
||||
print(f" 출력 경로: {DIST_DIR}")
|
||||
print(f" 실행 파일: {os.path.join(DIST_DIR, __exe_name__ + '.exe')}")
|
||||
if iss_file:
|
||||
print(f" Inno Setup 스크립트: {iss_file}")
|
||||
print("=" * 60)
|
||||
else:
|
||||
print("\n빌드 실패!")
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 909 B |
Binary file not shown.
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"01": "공업/과학 및 사진용 및 농업/원예 및 임업용 화학제; 미가공 인조수지, 미가공 플라스틱; 소화 및 화재예방용 조성물; 조질제 및 땜납용 조제; 수피용 무두질제; 공업용 접착제; 퍼티 및 기타 페이스트 충전제; 퇴비, 거름, 비료; 산업용 및 과학용 생물학적 제제",
|
||||
"02": "페인트, 니스, 래커; 방청제 및 목재 보존제; 착색제, 염료; 인쇄, 표시 및 판화용 잉크; 미가공 천연수지; 도장용/장식용/인쇄용/미술용 금속박(箔) 및 금속분(紛)",
|
||||
"03": "비의료용 화장품 및 세면용품; 비의료용 치약; 향료, 에센셜 오일; 표백제 및 기타 세탁용 제제; 세정/광택 및 연마재",
|
||||
"04": "공업용 오일 및 그리스, 왁스; 윤활제; 먼지흡수제, 먼지습윤제 및 먼지흡착제; 연료 및 발광체; 조명용 양초 및 심지",
|
||||
"05": "약제, 의료용 및 수의과용 제제; 의료용 위생제; 의료용 또는 수의과용 식이요법 식품 및 제제, 유아용 식품; 인체용 및 동물용 식이보충제; 플라스터, 외상치료용 재료; 치과용 충전재료, 치과용 왁스; 소독제; 해충구제제; 살균제, 제초제",
|
||||
"06": "일반금속 및 그 합금, 광석; 금속제 건축 및 구축용 재료; 금속제 이동식 건축물; 비전기용 일반금속제 케이블 및 와이어; 소형금속제품; 저장 또는 운반용 금속제 용기; 금고",
|
||||
"07": "기계, 공작기계, 전동공구; 모터 및 엔진(육상차량용은 제외); 기계 커플링 및 전동장치 부품(육상차량용은 제외); 농기구(수동식 수공구는 제외); 부란기(孵卵器); 자동판매기",
|
||||
"08": "수동식 수공구 및 수동기구; 커틀러리; 휴대 무기(화기는 제외); 면도기",
|
||||
"09": "과학, 연구, 항법, 측량, 사진, 영화, 시청각, 광학, 계량, 측정, 신호, 탐지, 시험, 검사, 구명 및 교육용 기기; 전기 분배 또는 전기 사용의 전도, 전환, 변형, 축적, 조절 또는 통제를 위한 기기; 음향/영상 또는 데이터의 기록/전송/재생 또는 처리용 장치 및 기구; 기록 및 내려받기 가능한 미디어, 컴퓨터 소프트웨어, 빈 디지털 또는 아날로그 기록 및 저장매체; 동전작동식 기계장치; 금전등록기, 계산기; 컴퓨터 및 컴퓨터주변기기; 잠수복, 잠수마스크, 잠수용 귀마개, 다이버 및 수영용 노즈클립, 잠수용 장갑, 잠수용 호흡장치; 소화기기",
|
||||
"10": "외과용, 내과용, 치과용 및 수의과용 기계기구; 의지(義肢), 의안(義眼) 및 의치(義齒); 정형외과용품; 봉합용 재료; 장애인용 치료 및 재활보조장치; 안마기; 유아수유용 기기 및 용품; 성활동용 기기 및 용품",
|
||||
"11": "조명용, 가열용, 냉각용, 증기발생용, 조리용, 건조용, 환기용, 급수용, 위생용 장치 및 설비",
|
||||
"12": "수송기계기구; 육상, 항공 또는 해상을 통해 이동하는 수송수단",
|
||||
"13": "화기(火器); 탄약 및 발사체; 폭약; 폭죽",
|
||||
"14": "귀금속 및 그 합금; 보석, 귀석 및 반귀석; 시계용구",
|
||||
"15": "악기; 악보대 및 악기용 받침대; 지휘봉",
|
||||
"16": "종이 및 판지; 인쇄물; 제본재료; 사진; 문방구 및 사무용품(가구는 제외); 문방구용 또는 가정용 접착제; 제도용구 및 미술용 재료; 회화용 솔; 교재; 포장용 플라스틱제 시트, 필름 및 가방; 인쇄활자, 프린팅블록",
|
||||
"17": "미가공 및 반가공 고무, 구타페르카, 고무액(gum), 석면, 운모(雲母) 및 이들의 제품; 제조용 압출성형형태의 플라스틱 및 수지; 충전용, 마개용 및 절연용 재료; 비금속제 신축관, 튜브 및 호스",
|
||||
"18": "가죽 및 모조가죽; 수피; 수하물가방 및 운반용 가방; 우산 및 파라솔; 걷기용 지팡이; 채찍 및 마구(馬具); 동물용 목걸이, 가죽끈 및 의류",
|
||||
"19": "건축용 및 구축용 비금속제 건축재료; 건축용 비금속제 경질관(硬質管); 아스팔트, 피치, 타르 및 역청; 비금속제 이동식 건축물; 비금속제 기념물",
|
||||
"20": "가구, 거울, 액자; 보관 또는 운송용 비금속제 컨테이너; 미가공 또는 반가공 뼈, 뿔, 고래수염 또는 나전(螺鈿); 패각; 해포석(海泡石); 호박(琥珀)(원석)",
|
||||
"21": "가정용 또는 주방용 기구 및 용기; 조리기구 및 식기(포크, 나이프 및 스푼은 제외); 빗 및 스펀지; 솔(페인트 솔은 제외); 솔 제조용 재료; 청소용구; 비건축용 미가공 또는 반가공 유리; 유리제품, 도자기제품 및 토기제품",
|
||||
"22": "로프 및 노끈; 망(網); 텐트 및 타폴린; 직물제 또는 합성재료제 차양; 돛; 하역물운반용 및 보관용 포대; 충전재료(고무/플라스틱/종이 및 판지제는 제외); 직물용 미가공 섬유 및 그 대용품",
|
||||
"23": "직물용 실(絲)",
|
||||
"24": "직물 및 직물대용품; 가정용 린넨; 직물 또는 플라스틱제 커튼",
|
||||
"25": "의류, 신발, 모자",
|
||||
"26": "레이스, 장식용 끈 및 자수포, 의류장식용 리본 및 나비매듭리본; 단추, 훅 및 아이(hooks and eyes), 핀 및 바늘; 조화(造花); 머리장식품; 가발",
|
||||
"27": "카펫, 융단, 매트, 리놀륨 및 기타 바닥깔개용 재료; 비직물제 벽걸이",
|
||||
"28": "오락용구, 장난감; 비디오게임장치; 체조 및 스포츠용품; 크리스마스트리용 장식품",
|
||||
"29": "식육, 생선, 가금 및 엽조수; 고기진액; 보존처리/냉동/건조 및 조리된 과일 및 채소; 젤리, 잼, 콤폿; 달걀; 우유, 치즈, 버터, 요구르트 및 기타 유제품; 식용 유지(油脂)",
|
||||
"30": "커피, 차(茶), 코코아 및 그 대용물; 쌀, 파스타 및 국수; 타피오카 및 사고(sago); 곡분 및 곡물 조제품; 빵, 페이스트리 및 과자; 초콜릿; 아이스크림, 셔벗 및 기타 식용 얼음; 설탕, 꿀, 당밀(糖蜜); 식품용 이스트, 베이킹 파우더; 소금, 조미료, 향신료, 보존처리된 허브; 식초, 소스 및 기타 조미료; 얼음",
|
||||
"31": "미가공 농업, 수산양식, 원예 및 임업 생산물; 미가공 곡물 및 종자; 신선한 과실 및 채소, 신선한 허브; 살아 있는 식물 및 꽃; 구근(球根), 모종 및 재배용 곡물종자; 살아있는 동물; 동물용 사료 및 음료; 맥아",
|
||||
"32": "맥주; 비알코올성 음료; 광천수 및 탄산수; 과실음료 및 과실주스; 시럽 및 비알코올성 음료용 제제",
|
||||
"33": "알코올성 음료(맥주는 제외); 음료제조용 알코올성 제제",
|
||||
"34": "담배 및 대용담배; 권연 및 여송연; 흡연자용 전자담배 및 기화기; 흡연용구; 성냥",
|
||||
"35": "광고업; 사업관리/조직 및 경영업; 사무처리업",
|
||||
"36": "금융, 통화 및 은행업; 보험서비스업; 부동산업",
|
||||
"37": "건축서비스업; 설치 및 수리서비스업; 채광업/석유 및 가스 시추업",
|
||||
"38": "통신서비스업",
|
||||
"39": "운송업; 상품의 포장 및 보관업; 여행알선업",
|
||||
"40": "재료처리업; 폐기물 재생업; 공기 정화 및 물 처리업; 인쇄 서비스업; 음식 및 음료수 보존업",
|
||||
"41": "교육업; 훈련제공업; 연예오락업; 스포츠 및 문화활동업",
|
||||
"42": "과학적, 기술적 서비스업 및 관련 연구, 디자인업; 산업분석, 산업연구 및 산업디자인 서비스업; 품질 관리 및 인증 서비스업; 컴퓨터 하드웨어 및 소프트웨어의 디자인 및 개발업",
|
||||
"43": "식음료제공서비스업; 임시숙박시설업",
|
||||
"44": "의료업; 수의업; 인간 또는 동물을 위한 위생 및 미용업; 농업, 수산양식, 원예 및 임업 서비스업",
|
||||
"45": "법무서비스업; 유형의 재산 및 개인을 물리적으로 보호하기 위한 보안서비스업; 이성(異性) 소개업, 온라인 소셜 네트워킹 서비스업; 장례업; 베이비시팅업"
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
161
mainUI_SP.py
161
mainUI_SP.py
|
|
@ -1621,6 +1621,7 @@ class MAIN_GUI(QMainWindow):
|
|||
'tag_method': 'product_name', # 'product_name', 'ai', 'keyword'
|
||||
'tag_ai': False,
|
||||
'tag_by_product_name': True,
|
||||
'tag_lens': False,
|
||||
'delete_all_tags': False,
|
||||
'thumb': False,
|
||||
'thumb_represent': False,
|
||||
|
|
@ -6634,7 +6635,6 @@ class MAIN_GUI(QMainWindow):
|
|||
self.tag_method_combo.setObjectName("tag_method_combo")
|
||||
self.tag_method_combo.addItem("상품명기반생성", "product_name")
|
||||
self.tag_method_combo.addItem("AI태그생성", "ai")
|
||||
self.tag_method_combo.addItem("기존태그방식", "keyword")
|
||||
# VIP 전용: 렌즈기반 태그 옵션
|
||||
if self.is_vip_user():
|
||||
self.tag_method_combo.addItem("렌즈기반태그 (VIP)", "lens")
|
||||
|
|
@ -6645,13 +6645,11 @@ class MAIN_GUI(QMainWindow):
|
|||
tag_method_help_text = """
|
||||
<p><b>상품명기반생성 (기본)</b>: 상품명에서 앞 4단어를 추출하여 키워드 검색 후 태그 추가</p>
|
||||
<p><b>AI태그생성</b>: GPT를 이용한 네이버 스마트스토어 SEO 기준 태그 생성</p>
|
||||
<p><b>기존태그방식</b>: 쇼핑렌즈로 수집한 '팔린상품'의 태그를 가져오기</p>
|
||||
"""
|
||||
if self.is_vip_user():
|
||||
tag_method_help_text = """
|
||||
<p><b>상품명기반생성 (기본)</b>: 상품명에서 앞 4단어를 추출하여 키워드 검색 후 태그 추가</p>
|
||||
<p><b>AI태그생성</b>: GPT를 이용한 네이버 스마트스토어 SEO 기준 태그 생성</p>
|
||||
<p><b>기존태그방식</b>: 쇼핑렌즈로 수집한 '팔린상품'의 태그를 가져오기</p>
|
||||
<p><b>렌즈기반태그 (VIP)</b>: 쇼핑렌즈로 검색한 실제 판매상품의 태그를 직접 가져옵니다</p>
|
||||
"""
|
||||
self.tag_method_widget.enterEvent = lambda e: self.show_manual_html(
|
||||
|
|
@ -7857,6 +7855,22 @@ class MAIN_GUI(QMainWindow):
|
|||
self.selected_group_total_products.setAlignment(Qt.AlignCenter) # 가운데 정렬
|
||||
self.selected_group_total_products.setStyleSheet(self.get_modern_label_style("#2c3e50", "#e2e8f0"))
|
||||
|
||||
# 세션 정리 버튼 추가
|
||||
self.session_cleanup_button = QPushButton("세션 정리", self)
|
||||
self.session_cleanup_button.setFixedHeight(30)
|
||||
self.session_cleanup_button.clicked.connect(self.on_session_cleanup_button_clicked)
|
||||
self.session_cleanup_button.setToolTip("비활성 세션(1분 이상 활동 없음)을 정리합니다.\n정리 후 그룹 목록이 자동으로 새로고침됩니다.")
|
||||
self.session_cleanup_button.setStyleSheet(self.get_modern_button_style("orange"))
|
||||
self.session_cleanup_button.setEnabled(True) # 로그인 후 항상 활성화
|
||||
|
||||
# 그룹 Refresh 버튼 추가
|
||||
self.group_refresh_button = QPushButton("그룹 새로고침", self)
|
||||
self.group_refresh_button.setFixedHeight(30)
|
||||
self.group_refresh_button.clicked.connect(self.on_group_refresh_button_clicked)
|
||||
self.group_refresh_button.setToolTip("작업 그룹 목록을 서버에서 다시 가져옵니다.")
|
||||
self.group_refresh_button.setStyleSheet(self.get_modern_button_style("green"))
|
||||
self.group_refresh_button.setEnabled(False) # 브라우저 시작 후 활성화
|
||||
|
||||
self.group_change_button = QPushButton("그룹 선택", self)
|
||||
self.group_change_button.setFixedHeight(30)
|
||||
# self.group_change_button.setFixedWidth(120) # 드롭박스 크기 줄인 만큼 버튼 크기 확대
|
||||
|
|
@ -7867,7 +7881,9 @@ class MAIN_GUI(QMainWindow):
|
|||
|
||||
self.select_group_layout.addWidget(self.group_selector_label, 0, 0)
|
||||
self.select_group_layout.addWidget(self.group_selector, 0, 1)
|
||||
self.select_group_layout.addWidget(self.group_change_button, 0, 2)
|
||||
self.select_group_layout.addWidget(self.session_cleanup_button, 0, 2)
|
||||
self.select_group_layout.addWidget(self.group_refresh_button, 0, 3)
|
||||
self.select_group_layout.addWidget(self.group_change_button, 0, 4)
|
||||
self.select_group_layout.addWidget(self.selected_group_label, 2, 0)
|
||||
self.select_group_layout.addWidget(self.selected_group, 2, 1)
|
||||
self.select_group_layout.addWidget(self.selected_group_total_products, 2, 2)
|
||||
|
|
@ -9362,6 +9378,11 @@ class MAIN_GUI(QMainWindow):
|
|||
"""브라우저 시작 완료 시 처리할 로직"""
|
||||
self.logger.log("브라우저가 성공적으로 시작되었습니다.", level=logging.INFO)
|
||||
|
||||
# 그룹 Refresh 버튼 활성화
|
||||
if hasattr(self, 'group_refresh_button') and self.group_refresh_button is not None:
|
||||
self.group_refresh_button.setEnabled(True)
|
||||
self.logger.log("그룹 새로고침 버튼이 활성화되었습니다.", level=logging.INFO)
|
||||
|
||||
# VIP 사용자의 경우 업로드정보삭제 버튼 활성화
|
||||
try:
|
||||
membership = (self.user_membership_level or 'free').lower()
|
||||
|
|
@ -9482,48 +9503,6 @@ class MAIN_GUI(QMainWindow):
|
|||
self.toggle_states['group_index'] = group_index
|
||||
self.logger.log(f"선택된 그룹이 변경되었습니다: {group_index}", level=logging.DEBUG)
|
||||
|
||||
# @Slot(list)
|
||||
# def update_group_list(self, group_names_list: list):
|
||||
# """
|
||||
# 그룹 이름 리스트를 업데이트합니다.
|
||||
# """
|
||||
# if not isinstance(group_names_list, list) or not group_names_list:
|
||||
# QMessageBox.critical(self, "오류", "그룹 선택에 실패했습니다. 프로그램을 재실행 해주세요.")
|
||||
# sys.exit(1)
|
||||
# else:
|
||||
# self.group_selector.clear()
|
||||
# self.group_selector.addItems(group_names_list)
|
||||
# self.group_selector.setEnabled(True)
|
||||
# self.group_change_button.setEnabled(True)
|
||||
|
||||
|
||||
# @Slot(list)
|
||||
# def update_group_list(self, group_names_list: list):
|
||||
# # supabase에서 현재 작업중인 그룹 목록 받아오기
|
||||
# active_sessions = self.supabase_manager.get_user_active_sessions(self.sp_user_id)
|
||||
|
||||
# # NULL 값과 빈 문자열을 제대로 필터링하여 실제 작업중인 그룹만 추출
|
||||
# active_groups = set()
|
||||
# for session in active_sessions:
|
||||
# selected_group = session.get("selected_group")
|
||||
# # NULL, None, 빈 문자열이 아닌 실제 값만 추가
|
||||
# if selected_group and selected_group.strip():
|
||||
# active_groups.add(selected_group.strip())
|
||||
|
||||
# self.logger.log(f"활성 세션에서 추출한 작업중인 그룹: {active_groups}", level=logging.DEBUG)
|
||||
|
||||
# # 이미 작업중인 그룹을 제외
|
||||
# filtered_groups = [g for g in group_names_list if g not in active_groups]
|
||||
|
||||
# if not filtered_groups:
|
||||
# QMessageBox.critical(self, "오류", f"그룹목록을 가져오는데 실패했습니다. \n 관리자에게 로그를 전송해주세요. \n 활성그룹 : {active_groups} \n 사용가능그룹 : {filtered_groups}")
|
||||
# sys.exit(1)
|
||||
# else:
|
||||
# self.group_selector.clear()
|
||||
# self.group_selector.addItems(filtered_groups)
|
||||
# self.group_selector.setEnabled(True)
|
||||
# self.group_change_button.setEnabled(True)
|
||||
|
||||
def clean_value(self, val):
|
||||
"""
|
||||
None, 빈 문자열, "null", "None", "NULL" 등 모두 빈 문자열로 처리.
|
||||
|
|
@ -9554,6 +9533,10 @@ class MAIN_GUI(QMainWindow):
|
|||
self.group_selector.setEnabled(True)
|
||||
if hasattr(self, 'group_change_button') and self.group_change_button is not None:
|
||||
self.group_change_button.setEnabled(True)
|
||||
# 그룹 Refresh 버튼 활성화 (브라우저가 실행 중인 경우)
|
||||
if hasattr(self, 'group_refresh_button') and self.group_refresh_button is not None:
|
||||
if hasattr(self, 'browser_controller') and self.browser_controller.isRunning():
|
||||
self.group_refresh_button.setEnabled(True)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -9610,6 +9593,11 @@ class MAIN_GUI(QMainWindow):
|
|||
self.group_selector.setEnabled(any_group_available)
|
||||
self.group_change_button.setEnabled(any_group_available)
|
||||
|
||||
# 그룹 Refresh 버튼 다시 활성화 (브라우저가 실행 중인 경우)
|
||||
if hasattr(self, 'group_refresh_button') and self.group_refresh_button is not None:
|
||||
if hasattr(self, 'browser_controller') and self.browser_controller.isRunning():
|
||||
self.group_refresh_button.setEnabled(True)
|
||||
|
||||
if not any_group_available:
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
|
|
@ -9892,6 +9880,87 @@ class MAIN_GUI(QMainWindow):
|
|||
self.logger.log(f"❌ 작업 시작 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||
QMessageBox.critical(self, "오류", f"작업 시작 중 오류가 발생했습니다:\n{e}")
|
||||
|
||||
@Slot()
|
||||
def on_session_cleanup_button_clicked(self):
|
||||
"""세션 정리 버튼 클릭 핸들러"""
|
||||
try:
|
||||
self.logger.log("세션 정리 시작", level=logging.INFO)
|
||||
|
||||
# 세션 정리 실행
|
||||
result = self.supabase_manager.cleanup_inactive_sessions(self.sp_user_id, inactive_minutes=1)
|
||||
|
||||
if result.get("success"):
|
||||
cleaned_count = result.get("cleaned_count", 0)
|
||||
message = result.get("message", "")
|
||||
|
||||
# 메시지 박스 표시
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"세션 정리 완료",
|
||||
message
|
||||
)
|
||||
|
||||
self.logger.log(f"세션 정리 완료: {cleaned_count}개 세션 정리됨", level=logging.INFO)
|
||||
|
||||
# 세션 정리 후 그룹 목록 새로고침 (브라우저가 실행 중인 경우에만)
|
||||
if hasattr(self, 'browser_controller') and self.browser_controller.isRunning():
|
||||
self.logger.log("세션 정리 후 그룹 목록 새로고침 시작 (2초 대기 후)", level=logging.INFO)
|
||||
# 데이터베이스 업데이트가 반영될 때까지 잠시 대기
|
||||
# 세션 정리 후 활성 세션 조회가 최신 상태를 반영하도록 지연
|
||||
def refresh_group_list():
|
||||
self.logger.log("세션 정리 후 그룹 목록 새로고침 실행", level=logging.INFO)
|
||||
self.browser_controller.get_group_names_list_all_task()
|
||||
QTimer.singleShot(2000, refresh_group_list)
|
||||
else:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"알림",
|
||||
"브라우저가 실행 중이지 않아 그룹 목록을 새로고침할 수 없습니다.\n브라우저를 시작한 후 '그룹 새로고침' 버튼을 눌러주세요."
|
||||
)
|
||||
else:
|
||||
error_message = result.get("message", "알 수 없는 오류")
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"세션 정리 실패",
|
||||
f"세션 정리 중 오류가 발생했습니다:\n{error_message}"
|
||||
)
|
||||
self.logger.log(f"세션 정리 실패: {error_message}", level=logging.ERROR)
|
||||
except Exception as e:
|
||||
self.logger.log(f"세션 정리 버튼 클릭 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"오류",
|
||||
f"세션 정리 중 예상치 못한 오류가 발생했습니다:\n{str(e)}"
|
||||
)
|
||||
|
||||
@Slot()
|
||||
def on_group_refresh_button_clicked(self):
|
||||
"""그룹 새로고침 버튼 클릭 핸들러"""
|
||||
try:
|
||||
if not hasattr(self, 'browser_controller') or not self.browser_controller.isRunning():
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
"알림",
|
||||
"브라우저가 실행 중이지 않습니다.\n먼저 '편집알바생 로그인' 버튼을 눌러 브라우저를 시작해주세요."
|
||||
)
|
||||
return
|
||||
|
||||
self.logger.log("그룹 목록 새로고침 시작", level=logging.INFO)
|
||||
self.group_refresh_button.setEnabled(False) # 중복 클릭 방지
|
||||
|
||||
# 그룹 목록 가져오기
|
||||
self.browser_controller.get_group_names_list_all_task()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"그룹 새로고침 버튼 클릭 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
QMessageBox.critical(
|
||||
self,
|
||||
"오류",
|
||||
f"그룹 목록 새로고침 중 예상치 못한 오류가 발생했습니다:\n{str(e)}"
|
||||
)
|
||||
if hasattr(self, 'group_refresh_button'):
|
||||
self.group_refresh_button.setEnabled(True)
|
||||
|
||||
def on_group_change_button_clicked(self):
|
||||
selected_text = self.group_selector.currentText()
|
||||
# "작업중" 문자열이 포함된 그룹을 선택했다면
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1767,22 +1767,32 @@ class OptionHandler:
|
|||
self.logger.log(f"입력 요소를 찾을 수 없어서 넘버링 건너뜀: {original_name}", level=logging.WARNING)
|
||||
continue
|
||||
|
||||
# 현재 입력값 가져오기 (넘버링 적용 전 값)
|
||||
# 번역된 값 가져오기 (Grok으로 정리된 값 우선 사용)
|
||||
translated_value = self.option_info.get("translated_names", {}).get(original_name)
|
||||
|
||||
# 번역된 값이 없거나 유효하지 않으면 현재 입력값 사용 (fallback)
|
||||
if not translated_value or not translated_value.strip():
|
||||
current_value = await name_input_elem.input_value()
|
||||
if not current_value:
|
||||
self.logger.log(f"입력값이 없어서 넘버링 건너뜀: {original_name}", level=logging.WARNING)
|
||||
continue
|
||||
translated_value = current_value.strip()
|
||||
self.logger.log(f"번역된 값이 없어서 현재 입력값 사용: {translated_value}", level=logging.DEBUG)
|
||||
else:
|
||||
translated_value = translated_value.strip()
|
||||
# 현재 입력값도 로그용으로 가져오기
|
||||
current_value = await name_input_elem.input_value()
|
||||
|
||||
# 넘버링 생성
|
||||
numbering = self.generate_numbering(numbering_method, global_index)
|
||||
|
||||
# 넘버링 적용된 최종 옵션명 생성
|
||||
final_option_name = f"{numbering}. {current_value.strip()}"
|
||||
# 넘버링 적용된 최종 옵션명 생성 (정리된 번역값 사용)
|
||||
final_option_name = f"{numbering}. {translated_value}"
|
||||
|
||||
# 실시간 순서 정보 저장 (get_all_translated_options용)
|
||||
real_time_option_info = {
|
||||
"original_name": original_name,
|
||||
"current_value": current_value.strip(), # 넘버링 적용 전 값
|
||||
"current_value": translated_value, # 넘버링 적용 전 값 (정리된 번역값)
|
||||
"final_option_name": final_option_name, # 넘버링 적용 후 값
|
||||
"numbering": numbering,
|
||||
"order": global_index
|
||||
|
|
@ -1793,10 +1803,10 @@ class OptionHandler:
|
|||
await name_input_elem.fill(final_option_name)
|
||||
|
||||
# 로그 및 저장 - 옵션명 변화 추적을 위한 상세 로그
|
||||
translated_value = self.option_info.get("translated_names", {}).get(original_name, "번역값없음")
|
||||
self.logger.log(f"메인 옵션 넘버링 적용: [{original_name}] → [{final_option_name}]", level=logging.INFO)
|
||||
self.logger.log(f" - 번역된값: {translated_value}", level=logging.DEBUG)
|
||||
self.logger.log(f" - 현재입력값: {current_value}", level=logging.DEBUG)
|
||||
self.logger.log(f" - 번역된값(정리됨): {translated_value}", level=logging.DEBUG)
|
||||
if current_value and current_value.strip() != translated_value:
|
||||
self.logger.log(f" - 현재입력값(무시됨): {current_value}", level=logging.DEBUG)
|
||||
self.logger.log(f" - 순서: {global_index}, 넘버링: {numbering}", level=logging.DEBUG)
|
||||
updated_final_options[original_name] = final_option_name
|
||||
|
||||
|
|
|
|||
|
|
@ -2702,6 +2702,139 @@ class SupabaseManager:
|
|||
"message": f"그룹 충돌 확인 중 오류가 발생했습니다: {str(e)}"
|
||||
}
|
||||
|
||||
def cleanup_inactive_sessions(self, user_id: str = None, inactive_minutes: int = 3) -> dict:
|
||||
"""
|
||||
is_active가 true이면서 last_active_time이 지정된 시간(기본 3분) 이상 지난 세션을 정리합니다.
|
||||
|
||||
Args:
|
||||
user_id (str): 사용자 ID (None이면 현재 사용자)
|
||||
inactive_minutes (int): 비활성으로 간주할 시간(분), 기본값 3분
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
"success": bool,
|
||||
"cleaned_count": int,
|
||||
"message": str
|
||||
}
|
||||
"""
|
||||
try:
|
||||
user_id = self.current_user_id if user_id is None else user_id
|
||||
|
||||
# 현재 시간에서 inactive_minutes 분 전 시간 계산
|
||||
cutoff_time = datetime.now(timezone.utc) - timedelta(minutes=inactive_minutes)
|
||||
cutoff_time_iso = cutoff_time.isoformat()
|
||||
|
||||
# 방법 1: JWT 인증으로 조회 및 업데이트 시도
|
||||
try:
|
||||
if not self.is_token_valid():
|
||||
self.reconnect()
|
||||
|
||||
# is_active가 true이고 last_active_time이 cutoff_time 이전인 세션 조회
|
||||
response = self.client.table("user_sessions").select("id, device_info, last_active_time").eq("user_id", user_id).eq("is_active", True).lt("last_active_time", cutoff_time_iso).execute()
|
||||
|
||||
if response.data:
|
||||
cleaned_count = 0
|
||||
for session in response.data:
|
||||
session_id = session.get("id")
|
||||
fields = {
|
||||
"is_active": False,
|
||||
"deactivated_reason": f"비활성 세션 정리 (마지막 활동: {session.get('last_active_time')})",
|
||||
"logout_time": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
if self.update_session_fields(session_id, fields):
|
||||
cleaned_count += 1
|
||||
self.logger.log(f"비활성 세션 정리 완료: {session_id} (기기: {session.get('device_info', '알 수 없음')})", level=logging.INFO)
|
||||
|
||||
message = f"{cleaned_count}개의 비활성 세션이 정리되었습니다."
|
||||
self.logger.log(f"비활성 세션 정리 완료 (JWT): {cleaned_count}개", level=logging.INFO)
|
||||
return {
|
||||
"success": True,
|
||||
"cleaned_count": cleaned_count,
|
||||
"message": message
|
||||
}
|
||||
else:
|
||||
self.logger.log("정리할 비활성 세션이 없습니다 (JWT)", level=logging.INFO)
|
||||
return {
|
||||
"success": True,
|
||||
"cleaned_count": 0,
|
||||
"message": "정리할 비활성 세션이 없습니다."
|
||||
}
|
||||
except Exception as jwt_error:
|
||||
self.logger.log(f"JWT 인증 방식 비활성 세션 정리 실패: {str(jwt_error)}", level=logging.WARNING)
|
||||
|
||||
# 방법 2: API 키 인증으로 조회 및 업데이트 시도
|
||||
try:
|
||||
api_url = f"{self.url}/rest/v1/user_sessions"
|
||||
headers = {
|
||||
"apikey": self.key,
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
# 비활성 세션 조회
|
||||
response = requests.get(
|
||||
f"{api_url}?user_id=eq.{user_id}&is_active=eq.true&last_active_time=lt.{cutoff_time_iso}&select=id,device_info,last_active_time",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
sessions = response.json()
|
||||
if sessions:
|
||||
cleaned_count = 0
|
||||
for session in sessions:
|
||||
session_id = session.get("id")
|
||||
fields = {
|
||||
"is_active": False,
|
||||
"deactivated_reason": f"비활성 세션 정리 (마지막 활동: {session.get('last_active_time')})",
|
||||
"logout_time": datetime.now(timezone.utc).isoformat()
|
||||
}
|
||||
|
||||
# 세션 업데이트
|
||||
update_response = requests.patch(
|
||||
f"{api_url}?id=eq.{session_id}",
|
||||
headers={
|
||||
"apikey": self.key,
|
||||
"Content-Type": "application/json",
|
||||
"Prefer": "return=representation"
|
||||
},
|
||||
json=fields
|
||||
)
|
||||
|
||||
if update_response.status_code in (200, 204):
|
||||
cleaned_count += 1
|
||||
self.logger.log(f"비활성 세션 정리 완료: {session_id} (기기: {session.get('device_info', '알 수 없음')})", level=logging.INFO)
|
||||
|
||||
message = f"{cleaned_count}개의 비활성 세션이 정리되었습니다."
|
||||
self.logger.log(f"비활성 세션 정리 완료 (API): {cleaned_count}개", level=logging.INFO)
|
||||
return {
|
||||
"success": True,
|
||||
"cleaned_count": cleaned_count,
|
||||
"message": message
|
||||
}
|
||||
else:
|
||||
self.logger.log("정리할 비활성 세션이 없습니다 (API)", level=logging.INFO)
|
||||
return {
|
||||
"success": True,
|
||||
"cleaned_count": 0,
|
||||
"message": "정리할 비활성 세션이 없습니다."
|
||||
}
|
||||
else:
|
||||
self.logger.log(f"API 키 방식 비활성 세션 조회 실패: HTTP {response.status_code}", level=logging.ERROR)
|
||||
except Exception as api_error:
|
||||
self.logger.log(f"API 키 방식 비활성 세션 정리 중 오류: {str(api_error)}", level=logging.ERROR)
|
||||
|
||||
return {
|
||||
"success": False,
|
||||
"cleaned_count": 0,
|
||||
"message": "비활성 세션 정리 중 오류가 발생했습니다."
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"비활성 세션 정리 중 오류: {str(e)}", level=logging.ERROR, exc_info=True)
|
||||
return {
|
||||
"success": False,
|
||||
"cleaned_count": 0,
|
||||
"message": f"비활성 세션 정리 중 오류가 발생했습니다: {str(e)}"
|
||||
}
|
||||
|
||||
def upload_logs_and_screenshots(self, date_str, logs_dir, error_screenshot_dir):
|
||||
"""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -2,10 +2,18 @@
|
|||
|
||||
### 패치 16 오류수정
|
||||
- 셔플모드에서도 중복,포함중복 등을 제거하도록 수정
|
||||
- 옵션명 정리 중 정리된 옵션명이 누락되는 문제 수정
|
||||
- 등록모드에서 스마트스토어에서 Failed to Fetch 문제 발생 수정
|
||||
|
||||
### 패치 16 기능개선
|
||||
- 쇼핑렌즈 추가(VIP전용)
|
||||
|
||||
- 쇼핑렌즈 사용시 이점
|
||||
: 상품 수집시(소싱) 잘못매칭된 상품이라도 타오바오 상품을 기준으로 네이버쇼핑 검색결과를 가져와 반영.
|
||||
: 실제팔린상품의 목록에서 광고제거, 판매이력, 리뷰수, 찜수, 태그(태그사전)를 가져와 정렬 후 반영.
|
||||
: 태그에 팔린상품에서 추출한 태그를 가져와 입력.
|
||||
- AI 모델 추가 (Grok-VIP전용)
|
||||
- 그룹세션 정리기능 추가(활성화 된지 3분이상 지난 모든 세션 정리)
|
||||
- 그룹세션 Refresh기능 추가
|
||||
|
||||
# 3.12.15 패치 업데이트 로그
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue