브라우저 컨트롤러에서 이미지 워커 재시작 로직을 활성화하고, 관련 주석을 정리하여 코드 가독성을 향상시켰습니다. 또한, UI에서 모든 태그 삭제 기능을 추가하고, 태그 처리 로직을 개선하여 사용자 경험을 향상시켰습니다. 버전 3.9.13으로 업데이트하며, 업데이트 로그에 변경 사항을 반영하였습니다.

This commit is contained in:
9700X_PC 2025-08-13 08:13:06 +09:00
parent 8bee9ef6a1
commit 1df87f005d
6 changed files with 433 additions and 15 deletions

View File

@ -0,0 +1,328 @@
; AutoPercenty3 Inno Setup Script
; 이 스크립트는 cx_Freeze로 빌드된 결과물이 있는 "build\exe.win-amd64-3.11" 폴더를 기반으로 인스톨러를 제작합니다.
; 20250802_205934에 생성됨
#define AppId "autopercenty"
#define MyAppName "Edit_PartTimer"
#define MyAppVersion "3.9.13"
#define MyAppPublisher "WhenRideMyCar"
#define MyAppProgramName "편집알바생"
#define MyAppDescription "편집알바생"
#define MyAppCopyright "Copyright 2024"
#define MyAppExeName "Edit_PartTimer3"
#define MySetupName "Edit_PartTimer Setup"
#define MySetupIcon "src/Edit_PartTimer3.ico"
#define MySetupOutputDir "dist/installer"
[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-1155"; Permissions: users-modify
; Playwright 브라우저 사용자폴더를 Program Files 내부에 생성
Name: "{app}\lib\src\browsers\user_data"; Permissions: users-modify
[Files]
; 프로그램 파일만 설치 (항상 덮어쓰기)
Source: "build\exe.win-amd64-3.11\*"; 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); // WM_QUIT
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); // 프로세스 종료 대기
end;
// 레지스트리에서 설치 경로 확인
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppName}_is1',
'InstallLocation', OldAppPath) then
begin
Log('기존 설치 경로: ' + OldAppPath);
// lib/src/user_data 폴더 백업
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);
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;

View File

@ -2924,14 +2924,14 @@ class BrowserController(QThread):
self.logger.log(f'{completed_count}/[{total_products}]개 상품 수정 완료.', level=logging.INFO)
# # --- (추가) N개마다 이미지 워커 재시작 ---
# if (completed_count % self.image_worker_restart_every) == 0:
# self.image_worker_mgr.restart(cause="periodic")
# self.logger.log(f"이미지 워커 재시작", level=logging.DEBUG)
# --- (추가) N개마다 이미지 워커 재시작 ---
if (completed_count % self.image_worker_restart_every) == 0:
self.image_worker_mgr.restart(cause="periodic")
self.logger.log(f"이미지 워커 재시작", level=logging.DEBUG)
#test
self.image_worker_mgr.restart(cause="periodic")
self.logger.log(f"이미지 워커 재시작", level=logging.DEBUG)
# #test
# self.image_worker_mgr.restart(cause="periodic")
# self.logger.log(f"이미지 워커 재시작", level=logging.DEBUG)
# # --- (추가) N개마다 컨텍스트 재시작 ---
@ -3846,9 +3846,9 @@ class ImageWorkerManager:
)
self._spawn_proc()
def _spawn_proc(self):
self.proc = Process(target=worker_main, args=self.proc_args, daemon=True)
self.proc.start()
# def _spawn_proc(self):
# self.proc = Process(target=worker_main, args=self.proc_args, daemon=True)
# self.proc.start()
# ───────── 새 프로세스 스폰 ─────────
def _spawn_proc(self):

View File

@ -794,6 +794,18 @@ class MAIN_GUI(QMainWindow):
"off": [],
}
},
"delete_all_tags_toggle": {
"key": "tag/delete_all_tags_toggle", # 태그 수정 사용 여부
"state_key": "delete_all_tags",
"dependents": { # ON/OFF별 enable 위젯 리스트
"on": [],
"off": []
},
"visible": { # ON/OFF별 visible 위젯 리스트
"on": [],
"off": [],
}
},
# 가격
"price_toggle": {
@ -1094,6 +1106,7 @@ class MAIN_GUI(QMainWindow):
'price': False,
'tag': False,
'tag_ai': False,
'delete_all_tags': False,
'thumb': False,
'thumb_trans_type': None,
'thumb_nukki': False,
@ -3452,6 +3465,33 @@ class MAIN_GUI(QMainWindow):
# 레이아웃에 위젯 추가
self.tags_toggle_layout.addWidget(self.tag_ai_widget)
# 태그 AI 생성 토글
self.delete_all_tags_widget = QWidget()
self.delete_all_tags_toggle_layout = QHBoxLayout(self.delete_all_tags_widget)
self.delete_all_tags_toggle_label = QLabel("모든 태그 삭제", self)
self.delete_all_tags_toggle = ToggleSwitch(self)
self.delete_all_tags_toggle.setOnText("삭제")
self.delete_all_tags_toggle.setOffText("유지")
self.delete_all_tags_toggle.setObjectName("delete_all_tags_toggle")
self.delete_all_tags_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.delete_all_tags_toggle, checked))
self.delete_all_tags_widget.enterEvent = lambda e: self.show_manual_html(
self.tag_manual_group,
"🏷️ 모든 태그 삭제",
self.tag_manual_label,
"""
<p>새로운 태그 입력 모든 태그를 삭제합니다.</p>
<p>기능 ON 모든 태그를 삭제합니다.</p>
<p>기능 OFF 기존 태그를 유지합니다.</p>
"""
)
self.delete_all_tags_widget.leaveEvent = lambda e: self.reset_manual(self.tag_manual_group, self.tag_manual_label)
self.delete_all_tags_toggle_layout.addWidget(self.delete_all_tags_toggle_label)
self.delete_all_tags_toggle_layout.addWidget(self.delete_all_tags_toggle)
# 레이아웃에 위젯 추가
self.tags_toggle_layout.addWidget(self.delete_all_tags_widget)
# 레이아웃에 그룹 추가
self.tag_layout.addWidget(self.tags_toggle_group, 3)
self.tag_layout.addWidget(self.tag_manual_group, 7)

View File

@ -16,6 +16,10 @@ class TagsHandler:
self.tags_input_locator = self.locator_manager.get_locator('TagsLocators', 'tags_input_locator')
self.tags_input_button_locator = self.locator_manager.get_locator('TagsLocators', 'tags_input_Button_locator')
self.delete_warning_tags_button_locator = self.locator_manager.get_locator('TagsLocators', 'delete_warning_tags_button_locator')
self.delete_all_tags_button_locator = self.locator_manager.get_locator('TagsLocators', 'delete_all_tags_button_locator')
self.warning_character_selector = self.locator_manager.get_locator('TagsLocators', 'warning_character_selector')
# CharacterDanger 경고 요소 선택자 (서버 처리 후 지연 발생 가능)
# self.warning_character_selector = 'div#productMainContentContainerId span.CharacterDanger'
def reset_state(self):
self.logger.log("TagsHandler 상태 초기화", level=logging.DEBUG)
@ -45,8 +49,8 @@ class TagsHandler:
# 입력 버튼 클릭
await self.click_input_button()
# 경고 키워드 삭제
await self.delete_warning_tags()
# 경고 키워드가 표시될 때까지 잠시 대기 후, 표시되면 삭제 버튼 클릭
await self.wait_for_warning_and_delete(timeout_ms=2000)
except TimeoutError as te:
self.logger.log(f"태그 처리 중 타임아웃 오류 발생: {te}", level=logging.ERROR, exc_info=True)
@ -96,14 +100,18 @@ class TagsHandler:
]
self.logger.log(f"Filtered keyword_tags List : {filtered_tags}", level=logging.DEBUG)
if self.toggle_states.get("delete_all_tags", False):
# 모든 태그 삭제
await self.delete_all_tags()
# 태그 입력
await self.enter_tags(filtered_tags)
# 입력 버튼 클릭
await self.click_input_button()
# 경고 키워드 삭제
await self.delete_warning_tags()
# 경고 키워드가 표시될 때까지 잠시 대기 후, 표시되면 삭제 버튼 클릭
await self.wait_for_warning_and_delete(timeout_ms=2000)
except TimeoutError as te:
self.logger.log(f"태그 처리 중 타임아웃 오류 발생: {te}", level=logging.ERROR, exc_info=True)
@ -167,6 +175,20 @@ class TagsHandler:
except Exception as e:
self.logger.log(f"태그 입력 버튼 클릭 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
async def delete_all_tags(self):
"""모든 태그 삭제 버튼 클릭."""
try:
self.logger.log("모든 태그 삭제 버튼 클릭 시도", level=logging.DEBUG)
delete_all_button = await self.page.query_selector(self.delete_all_tags_button_locator)
if delete_all_button is None:
raise Exception("모든 태그 삭제 버튼을 찾을 수 없습니다.")
await delete_all_button.click()
self.logger.log("모든 태그 삭제 버튼 클릭 완료", level=logging.INFO)
except Exception as e:
self.logger.log(f"모든 태그 삭제 버튼 클릭 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
async def delete_warning_tags(self):
"""경고 키워드 삭제 버튼 클릭."""
try:
@ -182,6 +204,24 @@ class TagsHandler:
except Exception as e:
self.logger.log(f"경고 키워드 삭제 버튼 클릭 중 오류 발생: {e}", level=logging.ERROR, exc_info=True)
async def wait_for_warning_and_delete(self, timeout_ms: int = 2000):
"""
'CharacterDanger' 경고 요소가 나타나는지 최대 timeout_ms(ms) 동안 대기한 ,
표시되면 경고 키워드 삭제를 수행하고, 아니면 조용히 건너뜁니다.
"""
try:
self.logger.log(
f"경고 키워드 표시 대기 시작 (최대 {timeout_ms}ms): selector={self.warning_character_selector}",
level=logging.DEBUG,
)
await self.page.wait_for_selector(self.warning_character_selector, state='visible', timeout=timeout_ms)
self.logger.log("경고 키워드 표시 감지됨. 삭제 버튼 클릭 시도", level=logging.INFO)
await self.delete_warning_tags()
except TimeoutError:
self.logger.log("경고 키워드 표시 없음(타임아웃). 삭제를 건너뜀", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"경고 키워드 대기/삭제 중 예외 발생: {e}", level=logging.ERROR, exc_info=True)
def _filter_chinese_characters(self, tags_list: List[str]) -> List[str]:
"""주어진 태그 리스트에서 중국어(한자) 문자가 포함된 태그를 제거합니다."""
chinese_pattern = re.compile(r"[\u3400-\u4DBF\u4E00-\u9FFF]")

View File

@ -7,7 +7,7 @@ import logging
""" 프로그램 기본 정보 """
__title__ = "Edit_PartTimer"
__description__ = "편집알바생"
__version__ = "3.9.12"
__version__ = "3.9.13"
__build__ = "1" # 빌드 번호
__author__ = "WhenRideMyCar"
__author_email__ = "abc@gmail.com"

View File

@ -1,6 +1,16 @@
- 비밀번호 변경 추가
# 3.9.13 업데이트 로그
### 오류 수정
- 이미지 워커 재시작주기가 일치하지 않는 문제 수정
### 기능 추가
- 키워드 탭에 기존태그 삭제 옵션 추가 (기존 태그를 모두 삭제 후 생성된 태그 입력)
# 3.9.12 업데이트 로그
### 오류 수정