브라우저 컨트롤러 및 UI 개선: ED 모드에 따른 이미지 프로세서 초기화 로직을 추가하고, 썸네일 및 상세페이지 수정 처리 로직을 개선하여 사용자 경험을 향상시켰습니다. 이미지 태그 제거 기능을 추가하여 등록모드에서의 텍스트 보존을 지원하며, 관련 로그 메시지를 개선하여 디버깅 용이성을 높였습니다. 데이터베이스 파일이 업데이트되었습니다.

This commit is contained in:
9700X_PC 2025-11-21 13:52:01 +09:00
parent 20d3aace50
commit 5d64b4f4e2
13 changed files with 689 additions and 96 deletions

View File

@ -0,0 +1,380 @@
; AutoPercenty3 Inno Setup Script
; 이 스크립트는 cx_Freeze로 빌드된 결과물이 있는 "build\exe.win-amd64-3.11" 폴더를 기반으로 인스톨러를 제작합니다.
; 20251106_104442에 생성됨
#define AppId "autopercenty"
#define MyAppName "Edit_PartTimer"
#define MyAppVersion "3.12.10"
#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
; updater.exe 파일 포함 (업데이트 관리자)
Source: "updater.exe"; DestDir: "{app}"; Flags: ignoreversion
; 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;
LibSrcPath: 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);
// 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);
// 1차 시도: Inno Setup 내장 함수
DeleteDir(OldAppPath);
// 삭제 확인 및 재시도
Sleep(2000); // 2초 대기
if DirExists(OldAppPath) then
begin
Log('설치 폴더 삭제 재시도 (Windows 명령어 사용): ' + OldAppPath);
// Windows rmdir 명령어로 강제 삭제 시도
Exec('cmd.exe', '/c rmdir /s /q "' + OldAppPath + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode);
Sleep(2000); // 추가 대기
end;
// 3차 시도: PowerShell로 강제 삭제
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 +
'설치를 계속하시겠습니까?' + #13#10 +
'(아니오를 선택하면 수동으로 폴더를 삭제한 후 다시 설치하세요)',
mbConfirmation, MB_YESNO) = IDNO then
begin
Result := False;
exit;
end;
end
else
begin
Log('기존 설치 폴더 삭제 완료: ' + OldAppPath);
end;
end
else
begin
Log('삭제할 설치 폴더가 존재하지 않음: ' + 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;

BIN
bizinfo.db Normal file

Binary file not shown.

View File

@ -550,14 +550,27 @@ class BrowserController(QThread):
if logger:
logger.log(f"{extension_id} 확장프로그램의 file:// 접근 권한을 자동 허용으로 설정했습니다.", level=20)
async def start_browser_async(self):
"""비동기 Playwright 초기화 및 로그인 수행"""
try:
if not self.toggle_states.get('ed_mode', False):
self.logger.log(f"일반모드로 이미지 프로세서 초기화 중...", level=logging.DEBUG)
# ED_MODE에서도 이미지 번역이 필요한 경우 이미지 프로세서 초기화
is_ed_mode = self.toggle_states.get('ed_mode', False)
detail_IMGTrans = self.toggle_states.get('detail_IMGTrans', False)
optionIMGTrans = self.toggle_states.get('optionIMGTrans', False)
thumb = self.toggle_states.get('thumb', False)
# 일반 모드이거나, ED_MODE에서 이미지 번역이 필요한 경우 초기화
needs_image_processor = not is_ed_mode or detail_IMGTrans or optionIMGTrans or thumb
if needs_image_processor:
if is_ed_mode:
self.logger.log(f"ED_MODE에서 이미지 번역이 필요하여 이미지 프로세서 초기화 중...", level=logging.DEBUG)
else:
self.logger.log(f"일반모드로 이미지 프로세서 초기화 중...", level=logging.DEBUG)
self.is_image_processor_init = self.init_image_processor()
if not self.is_image_processor_init:
self.logger.log(f"이미지 프로세서를 사용하지 않습니다.", level=logging.INFO)
self.image_processor_error.emit("이미지 프로세서 초기화 오류로 이미지번역이 실행되지 않습니다.")
@ -581,7 +594,7 @@ class BrowserController(QThread):
thumb_status = self.toggle_states.get('thumb', False)
debug_mode = self.toggle_states.get('debug_mode', False)
id_ed_mode = self.toggle_states.get('ed_mode', False)
id_ed_mode = is_ed_mode
if id_ed_mode:
debug_mode = True
self.logger.log(f"id_ed_mode: {id_ed_mode}", level=logging.DEBUG)
@ -4925,6 +4938,14 @@ class BrowserController(QThread):
self.gpt_client.update_gpt_model(self.toggle_states['gpt_model'])
# 이미지워커의 toggle_states 업데이트
if hasattr(self, 'image_processor') and self.image_processor:
try:
await self.image_processor.update_toggle_states(self.toggle_states)
self.logger.log("이미지워커 toggle_states 업데이트 완료", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"이미지워커 toggle_states 업데이트 중 오류: {e}", level=logging.WARNING)
# if not is_ed_mode:
# # 서버 URL 상태 체크 및 필요시 업데이트
@ -5133,9 +5154,20 @@ class BrowserController(QThread):
self.start_stage_signal.emit(3)
thumb = self.toggle_states.get('thumb')
thumb_nukki = self.toggle_states.get('thumb_nukki')
self.logger.log(f"썸네일수정 : {thumb} ", level=logging.DEBUG)
thumb_represent = self.toggle_states.get('thumb_represent', False)
# ED_MODE에서는 thumb_represent가 True일 때만, 일반 모드에서는 thumb 또는 thumb_nukki가 True일 때 실행
should_process_thumb = False
if is_ed_mode:
should_process_thumb = thumb_represent
if should_process_thumb:
self.logger.log(f"썸네일수정 (ED_MODE): thumb_represent={thumb_represent}", level=logging.DEBUG)
else:
should_process_thumb = thumb or thumb_nukki
if should_process_thumb:
self.logger.log(f"썸네일수정 : thumb={thumb}, thumb_nukki={thumb_nukki}", level=logging.DEBUG)
if thumb or thumb_nukki:
if should_process_thumb:
await self.random_human_behavior(self.page)
self.check_pause() # 일시중지 상태 확인
thumb_result = await self.edit_thumb(title_infos)
@ -5169,10 +5201,22 @@ class BrowserController(QThread):
detail_IMGTrans = self.toggle_states.get('detail_IMGTrans')
detail_IMGTrans_type = self.toggle_states.get('detail_IMGTrans_type')
# ED_MODE에서는 detail_IMGTrans만 허용 (detail_Option은 제외)
# 일반 모드에서는 detail_Option과 detail_IMGTrans 모두 허용
should_process_detail = False
if is_ed_mode:
# ED_MODE: 상세페이지 이미지 번역만 허용
should_process_detail = detail_IMGTrans
else:
# 일반 모드: 옵션과 이미지 번역 모두 허용
should_process_detail = detail_Option or detail_IMGTrans
if (detail_Option or detail_IMGTrans) and not is_ed_mode:
if should_process_detail:
self.check_pause() # 일시중지 상태 확인
self.logger.log(f"상세페이지 수정 : {detail_Option} + {detail_IMGTrans} + {detail_IMGTrans_type}", level=logging.DEBUG)
if is_ed_mode:
self.logger.log(f"상세페이지 수정 (ED_MODE): detail_IMGTrans={detail_IMGTrans}, type={detail_IMGTrans_type}", level=logging.DEBUG)
else:
self.logger.log(f"상세페이지 수정 : {detail_Option} + {detail_IMGTrans} + {detail_IMGTrans_type}", level=logging.DEBUG)
# 상세페이지 수정
await self.random_human_behavior(self.page)

View File

@ -1484,6 +1484,7 @@ class MAIN_GUI(QMainWindow):
'tag_by_product_name': True,
'delete_all_tags': False,
'thumb': False,
'thumb_represent': False,
'thumb_trans_type': None,
'thumb_nukki': False,
'remove_background_white': True,
@ -2060,7 +2061,14 @@ class MAIN_GUI(QMainWindow):
selected_value = "alphabetic_upper" # 기본값
self.logger.log(f"[옵션 넘버링 Debug] currentData가 비어있음, 기본값 사용: {selected_value}", level=logging.WARNING)
else:
selected_value = selected_text
# type: data 인 경우 currentData() 사용
config_type = config.get("type", "text")
if config_type == "data":
selected_value = widget.currentData()
if selected_value is None:
selected_value = selected_text
else:
selected_value = selected_text
# 설정 및 상태 저장
self.settings_manager.save_value(settings_key, selected_value)
@ -2071,11 +2079,9 @@ class MAIN_GUI(QMainWindow):
level=logging.DEBUG
)
# 의존성 있는 위젯 활성화/비활성화 처리 - GPT 모델의 경우 실제 데이터 값 전달
if widget_name == "gpt_model":
self.handle_toggle_state(widget, selected_value)
else:
self.handle_toggle_state(widget, selected_text)
# 의존성 있는 위젯 활성화/비활성화 처리 & 중복 저장(handle_toggle_state 내부)
# 모든 위젯에 대해 올바른 selected_value(데이터값) 전달
self.handle_toggle_state(widget, selected_value)
def handle_toggle_state(self, widget, value=None):
@ -2585,31 +2591,48 @@ class MAIN_GUI(QMainWindow):
def _load_toggle_states_from_settings(self):
"""SettingsManager에서 저장된 설정을 toggle_states에 로드합니다."""
try:
for key in self.toggle_states.keys():
# bool 타입 토글들
if isinstance(self.toggle_states[key], bool):
saved_value = self.settings_manager.get_value(f"toggle/{key}")
if saved_value is not None:
# QSettings에서 불러온 값을 bool로 변환
if isinstance(saved_value, str):
self.toggle_states[key] = saved_value.lower() in ['true', '1', 'yes']
else:
self.toggle_states[key] = bool(saved_value)
# 위젯맵을 통해 각 위젯의 저장된 값을 toggle_states에 로드
for widget_name, config in self.widget_map.items():
state_key = config.get("state_key")
if not state_key:
continue # state_key가 없는 위젯은 처리하지 않음
# 문자열 타입들
elif isinstance(self.toggle_states[key], str):
saved_value = self.settings_manager.get_value(f"input/{key}")
if saved_value is not None:
self.toggle_states[key] = str(saved_value)
# state_key가 toggle_states에 없는 경우 스킵
if state_key not in self.toggle_states:
continue
# 숫자 타입들
elif isinstance(self.toggle_states[key], (int, float)):
saved_value = self.settings_manager.get_value(f"spinbox/{key}")
if saved_value is not None:
if isinstance(self.toggle_states[key], int):
self.toggle_states[key] = int(saved_value)
else:
self.toggle_states[key] = float(saved_value)
# 위젯맵의 key를 사용하여 저장된 값 가져오기
settings_key = config.get("key")
if not settings_key:
continue
saved_value = self.settings_manager.get_value(settings_key)
if saved_value is None:
continue # 저장된 값이 없으면 스킵
# 타입에 맞게 변환하여 toggle_states 업데이트
current_value = self.toggle_states[state_key]
if isinstance(current_value, bool):
# bool 타입 변환
if isinstance(saved_value, str):
self.toggle_states[state_key] = saved_value.lower() in ['true', '1', 'yes']
else:
self.toggle_states[state_key] = bool(saved_value)
elif isinstance(current_value, str):
# 문자열 타입
self.toggle_states[state_key] = str(saved_value)
elif isinstance(current_value, (int, float)):
# 숫자 타입
if isinstance(current_value, int):
self.toggle_states[state_key] = int(saved_value)
else:
self.toggle_states[state_key] = float(saved_value)
self.logger.log(
f"[toggle_states 로드] {widget_name} (state_key: {state_key}): {self.toggle_states[state_key]}",
level=logging.DEBUG
)
self.logger.log("[SettingsManager] toggle_states 업데이트 완료", level=logging.DEBUG)
@ -3448,7 +3471,9 @@ class MAIN_GUI(QMainWindow):
# 기본 경로 사용 (내부에서 bizinfo.db 생성)
self.biz_dbManager = BizDBManager()
dlg = BizDialog(biz_db_manager=self.biz_dbManager, max_biz_count=3, parent=self, browser_controller=self.browser_controller)
self.max_biz_count = 20
dlg = BizDialog(biz_db_manager=self.biz_dbManager, max_biz_count=self.max_biz_count, parent=self, browser_controller=self.browser_controller)
dlg.exec()
# 버튼 라벨 갱신 (선택마켓 탭 사용으로 이름 표시는 유지)
@ -4299,7 +4324,7 @@ class MAIN_GUI(QMainWindow):
show_etc = False
show_upload_conditions = True # 등록모드에서는 항상 표시
if level == 'vip':
show_name, show_option, show_thumb, show_price, show_detail = True, False, True, True, False
show_name, show_option, show_thumb, show_price, show_detail = True, False, True, True, True
elif level == 'premium':
show_name, show_option, show_thumb, show_price, show_detail = True, False, False, True, False
else: # 기타 미분류는 basic로 취급
@ -4581,22 +4606,35 @@ class MAIN_GUI(QMainWindow):
self.logger.log(f"_hide_widgets_in_layout_by_names 오류: {e}", level=logging.WARNING)
def _update_vip_detail_edit_visibility(self):
"""VIP 상세페이지 수정 토글의 가시성을 업데이트합니다 (등록모드 + VIP 사용자만)"""
"""VIP 상세페이지 관련 토글의 가시성을 업데이트합니다 (등록모드 + VIP 사용자만)"""
try:
if hasattr(self, 'vip_detail_edit_widget'):
# 등록모드이고 VIP 사용자인 경우에만 표시
is_vip = (self.user_membership_level or '').lower() == 'vip'
show_vip_toggle = self.is_register_product_mode and is_vip
# 등록모드이고 VIP 사용자인 경우에만 표시
is_vip = (self.user_membership_level or '').lower() == 'vip'
show_vip_toggle = self.is_register_product_mode and is_vip
# # VIP 상세페이지 수정 토글
# if hasattr(self, 'vip_detail_edit_widget'):
# self.vip_detail_edit_widget.setVisible(show_vip_toggle)
self.vip_detail_edit_widget.setVisible(show_vip_toggle)
# if show_vip_toggle:
# self.logger.log("VIP 상세페이지 수정 토글 표시 (등록모드 + VIP 사용자)", level=logging.DEBUG)
# else:
# if not self.is_register_product_mode:
# self.logger.log("VIP 상세페이지 수정 토글 숨김 (기본모드)", level=logging.DEBUG)
# elif not is_vip:
# self.logger.log("VIP 상세페이지 수정 토글 숨김 (VIP 등급 아님)", level=logging.DEBUG)
# 상페 이미지 번역 토글
if hasattr(self, 'detail_IMGTrans_widget'):
self.detail_IMGTrans_widget.setVisible(show_vip_toggle)
if show_vip_toggle:
self.logger.log("VIP 상세페이지 수정 토글 표시 (등록모드 + VIP 사용자)", level=logging.DEBUG)
self.logger.log("상페 이미지 번역 토글 표시 (등록모드 + VIP 사용자)", level=logging.DEBUG)
else:
if not self.is_register_product_mode:
self.logger.log("VIP 상세페이지 수정 토글 숨김 (기본모드)", level=logging.DEBUG)
self.logger.log("상페 이미지 번역 토글 숨김 (기본모드)", level=logging.DEBUG)
elif not is_vip:
self.logger.log("VIP 상세페이지 수정 토글 숨김 (VIP 등급 아님)", level=logging.DEBUG)
self.logger.log("상페 이미지 번역 토글 숨김 (VIP 등급 아님)", level=logging.DEBUG)
except Exception as e:
self.logger.log(f"_update_vip_detail_edit_visibility 오류: {e}", level=logging.WARNING)
@ -6672,31 +6710,31 @@ class MAIN_GUI(QMainWindow):
self.detail_toggle_layout.addWidget(self.detail_text_button_widget)
# VIP용 등록모드 상세페이지 수정 토글
self.vip_detail_edit_widget = QWidget()
self.vip_detail_edit_toggle_layout = QHBoxLayout(self.vip_detail_edit_widget)
self.vip_detail_edit_toggle_label = QLabel("상페수정(등록모드)", self)
self.vip_detail_edit_toggle = ToggleSwitch(self)
self.vip_detail_edit_toggle.setEnabled(False) # 개선때 까지 비활성화
self.vip_detail_edit_toggle.setChecked(False) # 개선때 까지 비활성화
self.vip_detail_edit_toggle.setObjectName("vip_detail_edit_toggle")
self.vip_detail_edit_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.vip_detail_edit_toggle, checked))
self.vip_detail_edit_widget.enterEvent = lambda e: self.show_manual_html(
self.detail_manual_group,
"상세페이지 수정 (등록모드 전용)",
self.detail_manual_label,
"""
<h4>등록모드 상세페이지 수정</h4>
<p>등록모드에서 상세페이지를 수정할 있습니다.</p>
<p>활성화 선택마켓에서 지정된 스마트스토어의 <strong>사업자 이름 + 랜덤 홍보문구</strong> 줄에 자동 추가됩니다.</p>
"""
)
self.vip_detail_edit_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label)
# # VIP용 등록모드 상세페이지 수정 토글
# self.vip_detail_edit_widget = QWidget()
# self.vip_detail_edit_toggle_layout = QHBoxLayout(self.vip_detail_edit_widget)
# self.vip_detail_edit_toggle_label = QLabel("상페수정(등록모드)", self)
# self.vip_detail_edit_toggle = ToggleSwitch(self)
# self.vip_detail_edit_toggle.setEnabled(False) # 개선때 까지 비활성화
# self.vip_detail_edit_toggle.setChecked(False) # 개선때 까지 비활성화
# self.vip_detail_edit_toggle.setObjectName("vip_detail_edit_toggle")
# self.vip_detail_edit_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.vip_detail_edit_toggle, checked))
# self.vip_detail_edit_widget.enterEvent = lambda e: self.show_manual_html(
# self.detail_manual_group,
# "상세페이지 수정 (등록모드 전용)",
# self.detail_manual_label,
# """
# <h4>등록모드 상세페이지 수정</h4>
# <p>등록모드에서 상세페이지를 수정할 수 있습니다.</p>
# <p>활성화 시 선택마켓에서 지정된 스마트스토어의 <strong>사업자 이름 + 랜덤 홍보문구</strong>가 첫 줄에 자동 추가됩니다.</p>
# """
# )
# self.vip_detail_edit_widget.leaveEvent = lambda e: self.reset_manual(self.detail_manual_group, self.detail_manual_label)
self.vip_detail_edit_toggle_layout.addWidget(self.vip_detail_edit_toggle_label)
self.vip_detail_edit_toggle_layout.addWidget(self.vip_detail_edit_toggle)
# self.vip_detail_edit_toggle_layout.addWidget(self.vip_detail_edit_toggle_label)
# self.vip_detail_edit_toggle_layout.addWidget(self.vip_detail_edit_toggle)
self.detail_toggle_layout.addWidget(self.vip_detail_edit_widget)
# self.detail_toggle_layout.addWidget(self.vip_detail_edit_widget)
# 상페 이미지 번역 토글
self.detail_IMGTrans_widget = QWidget()

View File

@ -137,6 +137,69 @@ class DetailHandler:
self.logger.log(f"CKEditor 데이터 지우기 실패: {e}", level=logging.ERROR)
return False
def remove_image_tags_from_html(self, html_content: str) -> str:
"""HTML에서 이미지 태그만 제거하고 텍스트는 보존합니다.
Args:
html_content: 원본 HTML 문자열
Returns:
이미지 태그가 제거된 HTML 문자열
"""
try:
soup = BeautifulSoup(html_content, "html.parser")
# 모든 <img> 태그 찾아서 제거
for img in soup.find_all("img"):
img.decompose() # 태그와 내용 모두 제거
# <figure> 태그가 비어있으면 제거 (이미지와 함께 사용되는 경우)
for figure in soup.find_all("figure"):
if not figure.contents or len(figure.get_text(strip=True)) == 0:
figure.decompose()
cleaned_html = str(soup)
self.logger.log("이미지 태그 제거 완료 (텍스트 보존)", level=logging.DEBUG)
return cleaned_html
except Exception as e:
self.logger.log(f"이미지 태그 제거 중 오류: {e}", level=logging.ERROR)
return html_content # 오류 시 원본 반환
async def set_ckeditor_data(self, html_content: str):
"""CKEditor에 HTML 데이터를 설정합니다.
Args:
html_content: 설정할 HTML 문자열
Returns:
bool: 설정 성공 여부
"""
try:
# 소스 모드로 전환
await self.page.click(self.source_button_locator)
self.logger.log("소스 버튼 클릭 완료.", level=logging.DEBUG)
# 소스 편집 영역이 나타날 때까지 대기 (더 구체적인 선택자 사용)
await self.page.wait_for_selector(".ck-source-editing-area", timeout=5000)
# 소스 편집 영역만 선택 (strict mode violation 방지)
textarea_locator = self.page.locator(".ck-source-editing-area").first
await textarea_locator.wait_for(state="visible", timeout=5000)
# data-value 속성 설정
await textarea_locator.evaluate(f'(element) => {{ element.setAttribute("data-value", {repr(html_content)}); }}')
self.logger.log("CKEditor 데이터 설정 완료.", level=logging.DEBUG)
# 다시 에디터 모드로 전환
await self.page.click(self.source_button_locator)
self.logger.log("소스 버튼 재클릭 완료.", level=logging.DEBUG)
return True
except Exception as e:
self.logger.log(f"CKEditor 데이터 설정 실패: {e}", level=logging.ERROR)
return False
def is_valid_html(self, html_data: str) -> bool:
"""
@ -410,6 +473,7 @@ class DetailHandler:
self.detail_IMGTrans = toggle_states.get('detail_IMGTrans', False)
self.detail_IMGTrans_type = toggle_states.get('detail_IMGTrans_type', True)
self.unwanted_words = toggle_states.get('unwanted_words', False)
self.ed_mode = toggle_states.get('ed_mode', False) # 등록모드 확인
self.logger.log(f"self.toggle_states : {self.toggle_states}", level=logging.DEBUG)
@ -437,7 +501,8 @@ class DetailHandler:
# 상세페이지 옵션에 따라 처리
self.logger.log(f"self.detail_Option : {self.detail_Option}", level=logging.DEBUG)
self.logger.log(f"self.detail_IMGTrans : {self.detail_IMGTrans}", level=logging.DEBUG)
self.logger.log(f"image_processor 존재 여부: {self.browser_controller.image_processor is not None}", level=logging.DEBUG)
if (self.detail_Option or self.detail_IMGTrans) and (self.browser_controller.image_processor is not None):
# if self.detail_Option:
@ -453,15 +518,23 @@ class DetailHandler:
# if (self.detail_IMGTrans_type in ('CPU', '자체서버')) and (self.imageProcessor is not None):
if self.detail_IMGTrans_type and (self.browser_controller.image_processor is not None):
# CKEditor 내용 지우기
await self.clear_ckeditor_data()
# 등록모드(ed_mode) 처리 분기
if self.ed_mode:
# 등록모드: 기존 텍스트 보존, 이미지 태그만 제거
self.logger.log("등록모드: 이미지 태그만 제거하고 텍스트 보존", level=logging.INFO)
cleaned_html = self.remove_image_tags_from_html(current_html)
await self.set_ckeditor_data(cleaned_html)
else:
# 기본모드: CKEditor 내용 완전히 지우기
await self.clear_ckeditor_data()
# 텍스트 입력 필드 선택
# 텍스트 입력 필드 선택 (strict mode violation 방지)
input_field = self.page.locator(self.cke_text_editing_area_locator).first
await input_field.click()
self.logger.log("입력 필드 선택", level=logging.DEBUG)
# 소개글과 옵션데이터 입력
if self.detail_Option:
# 소개글과 옵션데이터 입력 (기본모드일 때만)
if self.detail_Option and not self.ed_mode:
await self.input_detail_text(optionHandler, input_field)
# 이미지 처리 통계
@ -481,9 +554,13 @@ class DetailHandler:
# 번역 파라미터 준비
font_type = self.browser_controller.toggle_states.get('font_type', '')
self.logger.log(f"font_type : {font_type}", level=logging.DEBUG)
unwanted_texts = list(self.browser_controller.unwanted_words.keys()) if hasattr(self.browser_controller, 'unwanted_words') else []
self.logger.log(f"unwanted_texts : {unwanted_texts}", level=logging.DEBUG)
is_member_valid = self.browser_controller.user_membership_level == "premium"
self.logger.log(f"is_member_valid : {is_member_valid}", level=logging.DEBUG)
authenticated_by_admin = self.browser_controller.authenticated_by_admin
self.logger.log(f"authenticated_by_admin : {authenticated_by_admin}", level=logging.DEBUG)
for idx, image_url in enumerate(image_urls):
try:
@ -674,7 +751,10 @@ class DetailHandler:
else:
# 상세페이지 옵션이 비활성화된 경우
self.logger.log("상세페이지 옵션이 비활성화되어 있습니다.", level=logging.INFO)
if self.browser_controller.image_processor is None:
self.logger.log("상세페이지 옵션이 활성화되어 있지만 image_processor가 초기화되지 않았습니다.", level=logging.WARNING)
else:
self.logger.log("상세페이지 옵션이 비활성화되어 있습니다.", level=logging.INFO)
return False
except Exception as e:

View File

@ -4,6 +4,7 @@ import os
import logging
import random
import re
import unicodedata
import math
import glob
@ -570,9 +571,6 @@ class OptionHandler:
비활성화: 마케팅 노이즈 제거, 오역방지 치환, Jieba 기반 사전체환, 색상 팔레트 통일
(요청사항: 색상/단위/크기·무게·전류치 스펙만 유지)
"""
import re
import unicodedata
if not isinstance(text, str) or not text.strip():
return text
@ -3359,7 +3357,6 @@ class OptionHandler:
def _extract_common_tokens(self, translations: list[str]) -> set:
"""여러 옵션명에서 공통으로 등장하는 토큰 집합을 추출"""
import re
if not translations:
return set()
token_sets = [set(re.split(r"\s+", txt)) for txt in translations if txt]
@ -3367,7 +3364,6 @@ class OptionHandler:
def _simplify_translation(self, text: str, common_tokens: set, max_length: int = 22) -> str:
"""공통어 제거, 중복 토큰 제거 후 지정 길이 내로 축약"""
import re
tokens = [tok for tok in re.split(r"\s+", text) if tok and tok not in common_tokens]
# 순서 보존하며 중복 제거
seen = set()
@ -3453,7 +3449,6 @@ class OptionHandler:
current_value = item_data['current_value']
# 기존 넘버링 패턴 제거 (정규식으로 다양한 넘버링 패턴 제거)
import re
# 다양한 넘버링 패턴을 제거하는 정규식
patterns = [
r'^[A-Za-z]+\.\s*', # A., B., AA., etc.
@ -3519,7 +3514,6 @@ class OptionHandler:
if match_mode == 'partial':
if forbidden_word.lower() in filtered_text.lower():
# 대소문자 무관하게 제거
import re
pattern = re.escape(forbidden_word)
filtered_text = re.sub(pattern, '', filtered_text, flags=re.IGNORECASE)
removed_words.append(forbidden_word)
@ -3532,7 +3526,6 @@ class OptionHandler:
removed_words.append(forbidden_word)
# 연속된 공백이나 특수문자 정리
import re
filtered_text = re.sub(r'\s+', ' ', filtered_text) # 연속 공백을 하나로
filtered_text = re.sub(r'[+\-\(\)]+\s*[+\-\(\)]+', '+', filtered_text) # 연속 구분자 정리
filtered_text = filtered_text.strip()
@ -3550,7 +3543,6 @@ class OptionHandler:
"""
텍스트에 한자가 포함되어 있는지 확인합니다.
"""
import re
if not isinstance(text, str):
return False
return bool(re.search(r'[\u4e00-\u9fff]', text))
@ -3559,7 +3551,6 @@ class OptionHandler:
"""
중국어 용어를 한국어로 번역하기 전에 일반적인 용어로 변환합니다.
"""
import re
if not isinstance(text, str):
return text
self.logger.log(f"최소 중국어 전처리 시작(라인 보존): {text}", level=logging.DEBUG)

View File

@ -114,10 +114,10 @@ class TitleGenerator:
self.parsing_page = parsing_page
self.logger.log(f"paparsing_pagege객체 업데이트 : {parsing_page}", level=logging.DEBUG)
def translate_product_name_from_google(self, original_name: str) -> str:
async def translate_product_name_from_google(self, original_name: str) -> str:
"""텍스트를 한국어로 번역하는 메서드"""
try:
translated_name = self.papago_translator.translate(original_name, 'ko')
translated_name = await self.papago_translator.translate(original_name, 'ko')
self.title_infos["translated_name"] = translated_name
return translated_name
except Exception as e:
@ -289,12 +289,12 @@ class TitleGenerator:
prefix += "/"
return prefix + title
def generate_product_title(self, trans_type: bool, original_name: str, keyword_name: str, search_result: dict, product_category: str, max_length: int = 35) -> str:
async def generate_product_title(self, trans_type: bool, original_name: str, keyword_name: str, search_result: dict, product_category: str, max_length: int = 35) -> str:
"""상품명을 생성하는 메서드"""
# 1. 원본 상품명 번역
if trans_type:
self.logger.log(f'trans_type : {trans_type}, 구글 번역 사용', level=logging.INFO)
translated_name = self.translate_product_name_from_google(original_name)
translated_name = await self.translate_product_name_from_google(original_name)
else:
self.logger.log(f'trans_type : {trans_type}, GPT 번역 사용', level=logging.INFO)
translated_name = self.translate_product_name(original_name)
@ -873,11 +873,12 @@ class TitleGenerator:
# 6. 상품명 생성
title_shuffle = self.toggle_states["title_shuffle"]
self.logger.log(f'상품명 셔플 여부 : {title_shuffle}', level=logging.DEBUG)
if title_shuffle:
product_title = self.process_title_shuffle(keyword_name=self.title_infos["keyword_name"], max_duplicates=1, max_length=self.toggle_states["title_length_limit"])
else:
generated_product_title = self.generate_product_title(trans_type=self.toggle_states["title_trans_type"], original_name=self.title_infos["original_name"], keyword_name=self.title_infos["keyword_name"], search_result=self.title_infos["search_result"], product_category=self.title_infos["category"])
generated_product_title = await self.generate_product_title(trans_type=self.toggle_states["title_trans_type"], original_name=self.title_infos["original_name"], keyword_name=self.title_infos["keyword_name"], search_result=self.title_infos["search_result"], product_category=self.title_infos["category"])
product_title = self.limit_title_length(generated_product_title, max_length=self.toggle_states["title_length_limit"])
if product_title == "관련성이 없는 상품 - 체크필요":
return

View File

@ -983,7 +983,7 @@ class BizDialog(QDialog):
self.selected_biz_id = 1
self.setWindowTitle("사업자 정보 입력")
# self.setGeometry(50, 50, 700, 600)
self.setMinimumSize(700, 600)
self.setMinimumSize(700, 700)
self.setMaximumSize(1000, 900)
self.max_biz_count = max_biz_count
self.current_biz_count = 0

View File

@ -247,6 +247,40 @@ class ImageWorkerClient:
except Exception as e:
return {"status": "error", "error": str(e)}
async def update_toggle_states(self, toggle_states: Dict[str, Any]) -> bool:
"""
이미지워커 서버의 toggle_states를 업데이트합니다.
Args:
toggle_states: 업데이트할 toggle_states 딕셔너리
Returns:
bool: 업데이트 성공 여부
"""
try:
payload = {
"toggle_states": toggle_states
}
r = requests.post(
f"{self.api}/v1/worker/update-toggle-states",
json=payload,
timeout=self.timeout
)
r.raise_for_status()
result = r.json()
if result.get("ok", False):
self.logger.log(f"이미지워커 toggle_states 업데이트 완료", level=logging.DEBUG)
return True
else:
self.logger.log(f"이미지워커 toggle_states 업데이트 실패: {result}", level=logging.WARNING)
return False
except requests.exceptions.RequestException as e:
self.logger.log(f"이미지워커 toggle_states 업데이트 중 네트워크 오류: {e}", level=logging.ERROR)
return False
except Exception as e:
self.logger.log(f"이미지워커 toggle_states 업데이트 중 오류: {e}", level=logging.ERROR, exc_info=True)
return False
# ---------------------- 제출/대기 ----------------------
async def submit_process_image(self, file_path: str, index: int, file_prefix: str,
font_type: str, unwanted_texts: List[str],

View File

@ -130,7 +130,17 @@ class SettingsManager:
:param widget_obj: 실제 위젯 객체(self )
"""
for widget_name, key in self.widget_map.items():
# widget_obj에 widget_map이 있으면 그것을 사용, 없으면 self.widget_map 사용
widget_map = getattr(widget_obj, 'widget_map', self.widget_map)
for widget_name, config in widget_map.items():
# config가 딕셔너리인 경우 (mainUI_SP.py 형식)
if isinstance(config, dict):
key = config.get("key", widget_name)
else:
# config가 문자열인 경우 (이전 형식)
key = config
widget = getattr(widget_obj, widget_name, None)
if widget is None:
self._log(f"[SettingsManager] '{widget_name}' 위젯을 찾을 수 없습니다.", logging.WARNING)

View File

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

View File

@ -2,6 +2,21 @@
- 비밀번호 변경 추가
# 3.12.11 패치 업데이트 로그
### 패치 11 오류수정
- 상품명 AI생성이 적용되지 않던 문제 적용
- 등록모드에서 대표썸네일 수정 선택사항이 반영되지 않던 문제 수정
### 패치 11 기능변경
- 등록모드에서도 상세페이지 번역기능 추가(VIP)
# 3.12.10 패치 업데이트 로그
### 패치 10 오류수정
- 옵션 유니코드 처리 오류 수정
# 3.12.9 패치 업데이트 로그
### 패치 9 오류수정
- 옵션자동선택을 꺼도 옵션이미지번역시 다시 동작하는 문제 수정