브라우저 컨트롤러 개선 및 업로드 조건 설정 로직 수정: 업로드 조건 설정 변수를 초기화하고, 관련 시그널을 연결하여 UI와의 연동을 강화하였습니다. 또한, ONNX 모델 로딩 및 GPU 사용 관련 안전성 개선을 위한 폴백 로직을 추가하였습니다. 데이터베이스 파일이 업데이트되었습니다.
This commit is contained in:
parent
fc0d7050e7
commit
71947fca83
|
|
@ -0,0 +1,378 @@
|
|||
; AutoPercenty3 Inno Setup Script
|
||||
; 이 스크립트는 cx_Freeze로 빌드된 결과물이 있는 "build\exe.win-amd64-3.11" 폴더를 기반으로 인스톨러를 제작합니다.
|
||||
; 20250927_210832에 생성됨
|
||||
|
||||
#define AppId "autopercenty"
|
||||
#define MyAppName "Edit_PartTimer"
|
||||
#define MyAppVersion "3.12.1"
|
||||
#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;
|
||||
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;
|
||||
|
|
@ -0,0 +1,378 @@
|
|||
; AutoPercenty3 Inno Setup Script
|
||||
; 이 스크립트는 cx_Freeze로 빌드된 결과물이 있는 "build\exe.win-amd64-3.11" 폴더를 기반으로 인스톨러를 제작합니다.
|
||||
; 20250928_000831에 생성됨
|
||||
|
||||
#define AppId "autopercenty"
|
||||
#define MyAppName "Edit_PartTimer"
|
||||
#define MyAppVersion "3.12.1"
|
||||
#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;
|
||||
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
BIN
bizinfo.db
Binary file not shown.
|
|
@ -137,6 +137,9 @@ class BrowserController(QThread):
|
|||
self.image_processor = None
|
||||
self.is_image_processor_init = False
|
||||
self._restart_in_progress: bool = False
|
||||
|
||||
# 마지막 선택된 그룹 기억하기 위한 변수
|
||||
self.last_selected_group = None
|
||||
|
||||
self.curr_page_idx: int = 1 # 1부터 시작
|
||||
self.curr_product_idx: int = 0 # 0-based, 현재 페이지 안에서 몇 번째 상품까지 끝냈는지
|
||||
|
|
@ -978,6 +981,247 @@ class BrowserController(QThread):
|
|||
self.browser_new_product_page_error.emit(str(e))
|
||||
return
|
||||
|
||||
async def is_registered_product_page(self):
|
||||
"""현재 페이지가 등록상품 페이지인지 확인 (3초 타임아웃)"""
|
||||
try:
|
||||
# 등록상품 페이지 식별자 확인 (3초 타임아웃)
|
||||
page_title_selector = "main.ant-layout-content div.ant-row-middle span.CharacterTitle85.H5Bold16"
|
||||
|
||||
try:
|
||||
# 3초 타임아웃으로 요소 대기
|
||||
element = await self.page.wait_for_selector(page_title_selector, timeout=3000)
|
||||
text = (await element.inner_text()).strip()
|
||||
is_registered_page = text == '등록 상품 목록'
|
||||
self.logger.log(f"등록상품 페이지 확인: '{text}' = {is_registered_page}", level=logging.DEBUG)
|
||||
return is_registered_page
|
||||
except Exception:
|
||||
# 타임아웃 또는 요소 없음
|
||||
self.logger.log("등록상품 페이지 식별자 요소를 찾을 수 없습니다. (3초 타임아웃)", level=logging.DEBUG)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(f"등록상품 페이지 확인 중 오류: {e}", level=logging.DEBUG)
|
||||
return False
|
||||
|
||||
async def is_new_product_page(self):
|
||||
"""현재 페이지가 신규상품 페이지인지 확인 (3초 타임아웃)"""
|
||||
try:
|
||||
# 신규상품 페이지 식별자 확인 (3초 타임아웃)
|
||||
page_title_selector = "main.ant-layout-content div.ant-row-middle[style='row-gap: 0px;'] span.Body1Bold14"
|
||||
|
||||
try:
|
||||
# 3초 타임아웃으로 요소 대기
|
||||
element = await self.page.wait_for_selector(page_title_selector, timeout=3000)
|
||||
text = (await element.inner_text()).strip()
|
||||
is_new_page = text == '수집 상품 목록'
|
||||
self.logger.log(f"신규상품 페이지 확인: '{text}' = {is_new_page}", level=logging.DEBUG)
|
||||
return is_new_page
|
||||
except Exception:
|
||||
# 타임아웃 또는 요소 없음
|
||||
self.logger.log("신규상품 페이지 식별자 요소를 찾을 수 없습니다. (3초 타임아웃)", level=logging.DEBUG)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(f"신규상품 페이지 확인 중 오류: {e}", level=logging.DEBUG)
|
||||
return False
|
||||
|
||||
async def is_market_settings_page(self):
|
||||
"""현재 페이지가 마켓설정 페이지인지 확인 (메뉴 선택 상태로 판단, 3초 타임아웃)"""
|
||||
try:
|
||||
# 1. 메뉴 아이템의 선택 상태로 확인 (가장 정확한 방법)
|
||||
# 선택된 마켓설정 메뉴 아이템 확인
|
||||
# 패턴: ul[role='menu'] li.ant-menu-item.ant-menu-item-selected[data-menu-id='rc-menu-uuid-11100-1-MARKET_SETTING'][role='menuitem']
|
||||
selected_market_menu_selector = "ul[role='menu'] li.ant-menu-item.ant-menu-item-selected[data-menu-id*='MARKET_SETTING'][role='menuitem']"
|
||||
|
||||
try:
|
||||
selected_element = await self.page.wait_for_selector(selected_market_menu_selector, timeout=3000)
|
||||
if selected_element:
|
||||
# 메뉴 ID 확인해서 로그 출력
|
||||
menu_id = await selected_element.get_attribute('data-menu-id')
|
||||
self.logger.log(f"마켓설정 페이지 확인 (메뉴 선택됨): {menu_id}", level=logging.DEBUG)
|
||||
return True
|
||||
except Exception:
|
||||
# 선택된 마켓설정 메뉴가 없음
|
||||
pass
|
||||
|
||||
# 2. 백업 방법: 마켓설정 메뉴 아이템 존재하지만 선택되지 않은 경우 확인
|
||||
# 패턴: ul[role='menu'] li.ant-menu-item[data-menu-id='rc-menu-uuid-11100-1-MARKET_SETTING'][role='menuitem']
|
||||
unselected_market_menu_selector = "ul[role='menu'] li.ant-menu-item:not(.ant-menu-item-selected)[data-menu-id*='MARKET_SETTING'][role='menuitem']"
|
||||
try:
|
||||
unselected_element = await self.page.wait_for_selector(unselected_market_menu_selector, timeout=1000)
|
||||
if unselected_element:
|
||||
# 메뉴가 존재하지만 선택되지 않은 경우는 마켓설정 페이지가 아님
|
||||
menu_id = await unselected_element.get_attribute('data-menu-id')
|
||||
self.logger.log(f"마켓설정 메뉴는 존재하지만 선택되지 않음: {menu_id}", level=logging.DEBUG)
|
||||
return False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. 백업 방법: URL로 확인
|
||||
current_url = self.page.url
|
||||
if 'market' in current_url.lower() and 'setting' in current_url.lower():
|
||||
self.logger.log(f"마켓설정 페이지 확인 (URL): {current_url}", level=logging.DEBUG)
|
||||
return True
|
||||
|
||||
# 4. 최종 백업: 페이지 타이틀로 확인
|
||||
try:
|
||||
page_title_selectors = [
|
||||
"main.ant-layout-content h1",
|
||||
"main.ant-layout-content .ant-typography-title"
|
||||
]
|
||||
|
||||
for selector in page_title_selectors:
|
||||
try:
|
||||
element = await self.page.wait_for_selector(selector, timeout=1000)
|
||||
if element:
|
||||
text = (await element.inner_text()).strip()
|
||||
if any(keyword in text for keyword in ['마켓', '설정', 'market', 'setting']):
|
||||
self.logger.log(f"마켓설정 페이지 확인 (제목): '{text}'", level=logging.DEBUG)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.logger.log("마켓설정 페이지가 아닙니다.", level=logging.DEBUG)
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"마켓설정 페이지 확인 중 오류: {e}", level=logging.DEBUG)
|
||||
return False
|
||||
|
||||
async def get_current_selected_group_name(self, ed_mode):
|
||||
"""현재 선택된 그룹 이름을 가져옴"""
|
||||
try:
|
||||
if ed_mode:
|
||||
group_selector = self.selected_group_name_for_ed_locator
|
||||
else:
|
||||
group_selector = self.selected_group_name_locator
|
||||
|
||||
group_name = (await self.page.inner_text(group_selector)).strip()
|
||||
self.logger.log(f"현재 선택된 그룹: '{group_name}'", level=logging.DEBUG)
|
||||
return group_name
|
||||
except Exception as e:
|
||||
self.logger.log(f"현재 그룹 이름 확인 실패: {e}", level=logging.DEBUG)
|
||||
return None
|
||||
|
||||
async def click_refresh_button(self):
|
||||
"""페이지 새로고침 버튼 클릭"""
|
||||
try:
|
||||
# 새로고침 버튼 - button 요소를 우선으로 시도
|
||||
refresh_button_selector = "main.ant-layout-content div.ant-row-middle button[type='button']:has(span[aria-label='reload'][role='img'])"
|
||||
|
||||
# 새로고침 버튼 존재 확인
|
||||
refresh_button = await self.page.query_selector(refresh_button_selector)
|
||||
if refresh_button:
|
||||
await refresh_button.click()
|
||||
self.logger.log("✅ 새로고침 버튼 클릭 완료", level=logging.INFO)
|
||||
await asyncio.sleep(2) # 새로고침 완료 대기
|
||||
return True
|
||||
else:
|
||||
# 대안: span 요소 직접 클릭
|
||||
span_selector = "main.ant-layout-content div.ant-row-middle button[type='button'] span[aria-label='reload'][role='img']"
|
||||
refresh_span = await self.page.query_selector(span_selector)
|
||||
if refresh_span:
|
||||
await refresh_span.click()
|
||||
self.logger.log("✅ 새로고침 아이콘 클릭 완료", level=logging.INFO)
|
||||
await asyncio.sleep(2) # 새로고침 완료 대기
|
||||
return True
|
||||
else:
|
||||
self.logger.log("❌ 새로고침 버튼을 찾을 수 없습니다.", level=logging.WARNING)
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.log(f"새로고침 버튼 클릭 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
return False
|
||||
|
||||
async def ensure_proper_page(self, ed_mode):
|
||||
"""
|
||||
ed_mode에 따라 적절한 페이지로 이동하고, 기존 선택 그룹도 복원
|
||||
- ed_mode=True: 등록상품 페이지로 이동
|
||||
- ed_mode=False: 신규상품 페이지로 이동
|
||||
- 마켓설정 페이지에서도 적절한 상품 페이지로 이동
|
||||
- 페이지 이동 후 이전에 선택했던 그룹으로 자동 선택
|
||||
"""
|
||||
try:
|
||||
current_url = self.page.url
|
||||
self.logger.log(f"현재 페이지 확인 중... (ed_mode: {ed_mode}, URL: {current_url})", level=logging.DEBUG)
|
||||
|
||||
# 먼저 마켓설정 페이지인지 확인
|
||||
is_market_settings = await self.is_market_settings_page()
|
||||
need_page_change = False
|
||||
current_group_name = None
|
||||
|
||||
if is_market_settings:
|
||||
self.logger.log("📋 현재 마켓설정 페이지입니다. 상품 관리 페이지로 이동합니다.", level=logging.INFO)
|
||||
need_page_change = True
|
||||
# 마켓설정 페이지에는 그룹이 없으므로 마지막 선택된 그룹 사용
|
||||
current_group_name = self.last_selected_group
|
||||
if current_group_name:
|
||||
self.logger.log(f"📋 마켓설정 페이지: 마지막 선택 그룹 '{current_group_name}' 사용", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log("📋 마켓설정 페이지: 마지막 선택 그룹 정보 없음, 기본 그룹으로 시작", level=logging.INFO)
|
||||
else:
|
||||
# 상품 페이지에서 현재 선택된 그룹 이름 저장 (페이지 이동 전)
|
||||
current_group_name = await self.get_current_selected_group_name(ed_mode)
|
||||
|
||||
if ed_mode:
|
||||
# 등록상품 모드 - 등록상품 페이지 확인
|
||||
if is_market_settings or not await self.is_registered_product_page():
|
||||
if not is_market_settings:
|
||||
self.logger.log("❌ 등록상품 페이지가 아닙니다. 이동 중...", level=logging.INFO)
|
||||
await self.go_to_registered_product_page()
|
||||
need_page_change = True
|
||||
|
||||
# 이동 후 확인
|
||||
await asyncio.sleep(2) # 페이지 로딩 대기
|
||||
if not await self.is_registered_product_page():
|
||||
self.logger.log("❌ 등록상품 페이지 이동 실패", level=logging.ERROR)
|
||||
return False
|
||||
self.logger.log("✅ 등록상품 페이지로 이동 완료", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log("✅ 이미 등록상품 페이지에 있습니다.", level=logging.DEBUG)
|
||||
else:
|
||||
# 신규상품 모드 - 신규상품 페이지 확인
|
||||
if is_market_settings or not await self.is_new_product_page():
|
||||
if not is_market_settings:
|
||||
self.logger.log("❌ 신규상품 페이지가 아닙니다. 이동 중...", level=logging.INFO)
|
||||
await self.go_to_new_product_page()
|
||||
need_page_change = True
|
||||
|
||||
# 이동 후 확인
|
||||
await asyncio.sleep(2) # 페이지 로딩 대기
|
||||
if not await self.is_new_product_page():
|
||||
self.logger.log("❌ 신규상품 페이지 이동 실패", level=logging.ERROR)
|
||||
return False
|
||||
self.logger.log("✅ 신규상품 페이지로 이동 완료", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log("✅ 이미 신규상품 페이지에 있습니다.", level=logging.DEBUG)
|
||||
|
||||
# 페이지 이동이 발생했고, 이전에 선택된 그룹이 있으면 복원
|
||||
if need_page_change and current_group_name and current_group_name != "전체":
|
||||
self.logger.log(f"🔄 이전 선택 그룹 '{current_group_name}' 복원 중...", level=logging.INFO)
|
||||
try:
|
||||
await self.select_group_by_name(current_group_name)
|
||||
self.logger.log(f"✅ 그룹 '{current_group_name}' 복원 완료", level=logging.INFO)
|
||||
except Exception as group_error:
|
||||
self.logger.log(f"⚠️ 그룹 '{current_group_name}' 복원 실패: {group_error}", level=logging.WARNING)
|
||||
# 그룹 선택 실패해도 페이지 이동은 성공했으므로 True 반환
|
||||
elif need_page_change and is_market_settings:
|
||||
if current_group_name and current_group_name != "전체":
|
||||
# 마켓설정 페이지에서 이동 후 마지막 선택 그룹 복원
|
||||
self.logger.log(f"📋 마켓설정에서 상품페이지로 이동 후 마지막 선택 그룹 '{current_group_name}' 복원 시도", level=logging.INFO)
|
||||
try:
|
||||
await self.select_group_by_name(current_group_name)
|
||||
self.logger.log(f"✅ 마켓설정에서 이동 후 그룹 '{current_group_name}' 복원 완료", level=logging.INFO)
|
||||
except Exception as group_error:
|
||||
self.logger.log(f"⚠️ 마켓설정에서 이동 후 그룹 '{current_group_name}' 복원 실패: {group_error}", level=logging.WARNING)
|
||||
else:
|
||||
self.logger.log("📋 마켓설정에서 이동했지만 마지막 선택 그룹 정보가 없어 기본 그룹으로 시작합니다.", level=logging.INFO)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"적절한 페이지 확인 및 이동 중 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
# async def start_browser(self):
|
||||
# """크롬 브라우저 실행 및 페이지 로딩"""
|
||||
|
|
@ -1945,10 +2189,28 @@ class BrowserController(QThread):
|
|||
max_steps = 400
|
||||
|
||||
for step in range(max_steps):
|
||||
# 현재 활성화 항목 텍스트 읽기
|
||||
active = self.page.locator(".ant-select-item-option-active")
|
||||
if await active.count() > 0:
|
||||
text = (await active.inner_text()).strip()
|
||||
# 현재 활성화 항목 텍스트 읽기 - 더 구체적인 선택자 사용
|
||||
active_elements = self.page.locator(".ant-select-item-option-active")
|
||||
active_count = await active_elements.count()
|
||||
|
||||
if active_count > 0:
|
||||
# 여러 개의 active 요소가 있을 경우, 그룹 관련 요소만 선택
|
||||
text = None
|
||||
for i in range(active_count):
|
||||
element = active_elements.nth(i)
|
||||
element_text = (await element.inner_text()).strip()
|
||||
|
||||
# 페이지당 상품수 드롭다운 항목은 무시 ("개씩 보기"가 포함된 것들)
|
||||
if "개씩 보기" not in element_text:
|
||||
text = element_text
|
||||
break
|
||||
|
||||
if text is None:
|
||||
# 그룹 관련 요소를 찾지 못한 경우, 첫 번째 요소 사용
|
||||
text = (await active_elements.first().inner_text()).strip()
|
||||
|
||||
self.logger.log(f"현재 활성 항목: '{text}' (전체 {active_count}개 중)", level=logging.DEBUG)
|
||||
|
||||
if text == group_name:
|
||||
await self.page.keyboard.press("Enter")
|
||||
option_found = True
|
||||
|
|
@ -1971,6 +2233,8 @@ class BrowserController(QThread):
|
|||
|
||||
if option_found:
|
||||
self.logger.log(f"✅ 그룹 '{group_name}' 선택 완료", level=logging.INFO)
|
||||
# 마지막 선택된 그룹 기억
|
||||
self.last_selected_group = group_name
|
||||
# 총 상품 개수 가져오기
|
||||
product_count_info = await self.get_total_product_count()
|
||||
total_products = product_count_info.get("total_count", 0)
|
||||
|
|
@ -2809,7 +3073,7 @@ class BrowserController(QThread):
|
|||
self.logger.log("최대 스크롤 횟수에 도달했습니다.", level=logging.DEBUG)
|
||||
|
||||
|
||||
async def collect_product_info(self, items_per_page, ed_mode):
|
||||
async def collect_product_info(self, items_per_page, ed_mode, total_products=None):
|
||||
"""
|
||||
상품 정보를 수집하는 메서드
|
||||
"""
|
||||
|
|
@ -2817,6 +3081,12 @@ class BrowserController(QThread):
|
|||
product_infos = []
|
||||
product_name_elements = [] # product_name_element를 저장할 리스트
|
||||
|
||||
# 실제 수집할 상품 개수 결정 (총 상품수와 페이지당 상품수 중 작은 값)
|
||||
actual_items_to_collect = items_per_page
|
||||
if total_products is not None:
|
||||
actual_items_to_collect = min(items_per_page, total_products)
|
||||
self.logger.log(f"실제 수집할 상품 개수: {actual_items_to_collect} (총 상품: {total_products}, 페이지당: {items_per_page})", level=logging.DEBUG)
|
||||
|
||||
# ed_mode에 따라 product_elements 설정
|
||||
if ed_mode:
|
||||
# 각 상품의 이름, 가격, 이미지를 위한 선택자 리스트 구성 (index가 2부터 시작)
|
||||
|
|
@ -2826,13 +3096,14 @@ class BrowserController(QThread):
|
|||
"price": self.product_price_for_ed_template.format(index=i),
|
||||
"image": self.product_image_for_ed_template.format(index=i)
|
||||
}
|
||||
for i in range(2, items_per_page + 2) # index가 2부터 시작하도록 설정
|
||||
for i in range(2, actual_items_to_collect + 2) # 실제 상품 개수만큼만 생성
|
||||
]
|
||||
else:
|
||||
# ed_mode=False일 때는 각 상품의 부모 요소를 모두 선택
|
||||
product_elements = await self.page.query_selector_all(self.product_parent_locator)
|
||||
all_product_elements = await self.page.query_selector_all(self.product_parent_locator)
|
||||
product_elements = all_product_elements[:actual_items_to_collect] # 실제 상품 개수만큼만 선택
|
||||
|
||||
for i, element in enumerate(product_elements[:items_per_page], start=1):
|
||||
for i, element in enumerate(product_elements, start=1):
|
||||
try:
|
||||
if ed_mode:
|
||||
# ed_mode=True일 때는 각 상품의 개별 선택자 사용
|
||||
|
|
@ -3978,6 +4249,12 @@ class BrowserController(QThread):
|
|||
- 삭제 완료 후 성과 로그 추출
|
||||
"""
|
||||
try:
|
||||
# 적절한 페이지로 이동 확인 (ed_mode용이므로 True)
|
||||
ed_mode = self.toggle_states.get('ed_mode', True) # ed_mode용 메서드이므로 기본값 True
|
||||
if not await self.ensure_proper_page(ed_mode):
|
||||
self.logger.log("❌ 적절한 페이지로 이동 실패", level=logging.ERROR)
|
||||
return
|
||||
|
||||
is_already_ext = await self.off_on_ext_Percenty()
|
||||
if not is_already_ext:
|
||||
self.logger.log("확장 페이지 열기 실패", level=logging.ERROR)
|
||||
|
|
@ -4167,6 +4444,11 @@ class BrowserController(QThread):
|
|||
self.complete_remove_market_info_job.emit(completion_msg)
|
||||
self.step_completed.emit("delete_upload_info", True)
|
||||
|
||||
# 업로드정보 삭제 완료 후 새로고침 버튼 클릭
|
||||
await asyncio.sleep(1) # 1초 대기
|
||||
self.logger.log("🔄 업로드정보 삭제 완료 후 새로고침 실행 중...", level=logging.INFO)
|
||||
await self.click_refresh_button()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"ed_bulk_delete_market_info 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
self.step_completed.emit("delete_upload_info", False)
|
||||
|
|
@ -4187,6 +4469,13 @@ class BrowserController(QThread):
|
|||
else:
|
||||
self.logger.log("🔧 업로드조건 미설정 - 기본값 사용", level=logging.INFO)
|
||||
|
||||
# 적절한 페이지로 이동 확인
|
||||
ed_mode = self.toggle_states.get('ed_mode', False)
|
||||
if not await self.ensure_proper_page(ed_mode):
|
||||
self.logger.log("❌ 적절한 페이지로 이동 실패", level=logging.ERROR)
|
||||
self.upload_log_message.emit("❌ 페이지 이동 실패")
|
||||
return
|
||||
|
||||
# 업로드 시작 시그널
|
||||
self.upload_step_changed.emit("상품 업로드 준비 중", 5)
|
||||
self.upload_log_message.emit("업로드 작업을 시작합니다...")
|
||||
|
|
@ -4250,6 +4539,10 @@ class BrowserController(QThread):
|
|||
# 총 상품수 정보 전달
|
||||
self.upload_progress_updated.emit(0, total_products)
|
||||
self.upload_log_message.emit(f"총 {total_products}개 상품, {total_pages}페이지를 처리합니다.")
|
||||
|
||||
# 업로드 통계 추적
|
||||
total_upload_success = 0
|
||||
total_upload_fail = 0
|
||||
|
||||
async def ensure_select_all_checked():
|
||||
"""전체상품 체크박스가 항상 체크 상태가 되도록 보장"""
|
||||
|
|
@ -4501,6 +4794,8 @@ class BrowserController(QThread):
|
|||
m = re.findall(r"(\d+)건 성공\s*/\s*(\d+)건 실패", text_content)
|
||||
if m:
|
||||
success_count, fail_count = m[0]
|
||||
total_upload_success += int(success_count)
|
||||
total_upload_fail += int(fail_count)
|
||||
msg = f"업로드 결과 - 성공: {success_count}건, 실패: {fail_count}건"
|
||||
self.logger.log(msg, level=logging.INFO)
|
||||
else:
|
||||
|
|
@ -4566,10 +4861,14 @@ class BrowserController(QThread):
|
|||
|
||||
self.logger.log("ed_mode 선택상품 일괄 업로드 완료", level=logging.INFO)
|
||||
|
||||
# 업로드 완료 시그널
|
||||
# 최종 업로드 통계를 포함한 완료 메시지 전달
|
||||
completion_msg = f"업로드 작업 완료 - 총 성공: {total_upload_success}건, 총 실패: {total_upload_fail}건"
|
||||
self.logger.log(completion_msg, level=logging.INFO)
|
||||
|
||||
# 업로드 완료 시그널 (통계 포함)
|
||||
self.upload_step_changed.emit("업로드 완료", 100)
|
||||
self.upload_log_message.emit("모든 상품 업로드가 성공적으로 완료되었습니다!")
|
||||
self.upload_completed.emit(True, "업로드 완료")
|
||||
self.upload_log_message.emit(completion_msg)
|
||||
self.upload_completed.emit(True, completion_msg)
|
||||
self.step_completed.emit("upload_products", True)
|
||||
|
||||
# ✅ 모든 작업 후 1페이지로 돌아가기
|
||||
|
|
@ -4588,6 +4887,12 @@ class BrowserController(QThread):
|
|||
|
||||
is_ed_mode = self.toggle_states.get('ed_mode', False)
|
||||
|
||||
# 적절한 페이지로 이동 확인
|
||||
if not await self.ensure_proper_page(is_ed_mode):
|
||||
self.logger.log("❌ 적절한 페이지로 이동 실패", level=logging.ERROR)
|
||||
self.translation_error.emit("페이지 이동 실패")
|
||||
return
|
||||
|
||||
self.running = True # 번역 작업이 시작됨
|
||||
self.translation_started.emit()
|
||||
|
||||
|
|
@ -4662,7 +4967,7 @@ class BrowserController(QThread):
|
|||
product_buttons = await self.get_product_edit_buttons_by_template()
|
||||
else:
|
||||
self.logger.log('상품정보 수집', level=logging.DEBUG)
|
||||
product_infos, product_name_elements = await self.collect_product_info(items_per_page, ed_mode=is_ed_mode)
|
||||
product_infos, product_name_elements = await self.collect_product_info(items_per_page, ed_mode=is_ed_mode, total_products=total_products)
|
||||
self.logger.log(f"product_infos : {product_infos}", level=logging.DEBUG)
|
||||
self.logger.log('수정모드이므로 상품명 elements를 수정버튼으로 활용합니다.', level=logging.DEBUG)
|
||||
product_buttons = [{"edit_button": name_element, "memo_button": None, "shipping_button": None} for name_element in product_name_elements]
|
||||
|
|
@ -5332,16 +5637,6 @@ class BrowserController(QThread):
|
|||
self.logger.log("이벤트 루프가 초기화되지 않았거나 이미 종료되었습니다.", level=logging.ERROR)
|
||||
|
||||
|
||||
def start_UploadSelectedMarketsJob_task(self):
|
||||
"""번역 작업을 이벤트 루프에서 실행"""
|
||||
# 이벤트 루프가 없거나 닫혀 있으면 새로 생성
|
||||
# self.initialize_event_loop()
|
||||
|
||||
if self.loop and not self.loop.is_closed():
|
||||
# 이미 실행 중인 이벤트 루프에 번역 작업 추가
|
||||
asyncio.run_coroutine_threadsafe(self.ed_bulk_upload_selected_markets(), self.loop)
|
||||
else:
|
||||
self.logger.log("이벤트 루프가 초기화되지 않았거나 이미 종료되었습니다.", level=logging.ERROR)
|
||||
|
||||
def cancel_upload(self):
|
||||
"""업로드 취소"""
|
||||
|
|
@ -5644,7 +5939,7 @@ class BrowserController(QThread):
|
|||
if not self.toggle_states['ed_mode']:
|
||||
product_buttons = await self.get_product_edit_buttons_by_template()
|
||||
else:
|
||||
product_infos, product_name_elements = await self.collect_product_info(items_per_page, ed_mode=self.toggle_states['ed_mode'])
|
||||
product_infos, product_name_elements = await self.collect_product_info(items_per_page, ed_mode=self.toggle_states['ed_mode'], total_products=total_products)
|
||||
product_buttons = [{"edit_button": name_element, "memo_button": None, "shipping_button": None} for name_element in product_name_elements]
|
||||
|
||||
return total_products, product_buttons
|
||||
|
|
@ -6093,6 +6388,174 @@ class BrowserController(QThread):
|
|||
return False, response_time
|
||||
|
||||
|
||||
|
||||
def start_UploadSelectedMarketsJob_task(self, upload_conditions=None):
|
||||
"""업로드 작업을 이벤트 루프에서 실행"""
|
||||
if self.loop and not self.loop.is_closed():
|
||||
# 이미 실행 중인 이벤트 루프에 업로드 작업 추가
|
||||
asyncio.run_coroutine_threadsafe(self.ed_bulk_upload_selected_markets(upload_conditions), self.loop)
|
||||
else:
|
||||
self.logger.log("이벤트 루프가 초기화되지 않았거나 이미 종료되었습니다.", level=logging.ERROR)
|
||||
|
||||
async def apply_upload_conditions_to_page(self, price_policy: int, smartstore_policy: bool):
|
||||
"""업로드조건을 웹페이지에 실제로 적용"""
|
||||
try:
|
||||
self.logger.log(f"🔧 웹페이지에 업로드조건 적용 시작 - 가격정책: {price_policy}, 스마트스토어정책: {smartstore_policy}", level=logging.INFO)
|
||||
|
||||
# 1. 가격정책 드롭다운 변경
|
||||
await self.apply_price_policy_dropdown(price_policy)
|
||||
|
||||
# 2. 스마트스토어정책 체크박스 변경
|
||||
await self.apply_smartstore_policy_checkbox(smartstore_policy)
|
||||
|
||||
self.logger.log("✅ 웹페이지 업로드조건 적용 완료", level=logging.INFO)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ 웹페이지 업로드조건 적용 실패: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
async def apply_price_policy_dropdown(self, price_policy: int):
|
||||
"""가격정책 드롭다운 변경 (1: 최대 업로드 갯수 기준, 2: 최저 옵션가 기준, 3: 상품별 각각의 대표가격 설정)"""
|
||||
try:
|
||||
# 가격정책 옵션 텍스트 매핑
|
||||
policy_options = {
|
||||
1: "최대 업로드 갯수 기준",
|
||||
2: "최저 옵션가 기준",
|
||||
3: "상품별 각각의 대표 가격 설정 적용"
|
||||
}
|
||||
|
||||
target_option = policy_options.get(price_policy, policy_options[1])
|
||||
self.logger.log(f"📋 가격정책 변경 시도: {target_option} (정책번호: {price_policy})", level=logging.DEBUG)
|
||||
|
||||
# 현재 선택된 값 확인
|
||||
current_selection = self.page.locator("div.ant-modal-content .ant-select-selection-item")
|
||||
try:
|
||||
current_text = await current_selection.inner_text(timeout=5000)
|
||||
if target_option in current_text:
|
||||
self.logger.log(f"✅ 가격정책이 이미 '{target_option}'로 설정되어 있음", level=logging.DEBUG)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 드롭다운 클릭하여 열기
|
||||
dropdown_selector = "div.ant-modal-content .ant-select"
|
||||
dropdown = self.page.locator(dropdown_selector)
|
||||
await dropdown.click()
|
||||
self.logger.log("📋 가격정책 드롭다운 열기", level=logging.DEBUG)
|
||||
|
||||
# 드롭다운 옵션 리스트 대기
|
||||
await asyncio.sleep(1) # 충분한 시간 확보
|
||||
|
||||
# 더 안전한 방식으로 옵션 선택: 드롭다운 내의 옵션들을 찾기
|
||||
option_elements = await self.page.query_selector_all(".ant-select-dropdown .ant-select-item")
|
||||
self.logger.log(f"📋 발견된 옵션 수: {len(option_elements)}", level=logging.DEBUG)
|
||||
|
||||
for i, option_element in enumerate(option_elements):
|
||||
try:
|
||||
option_text = await option_element.inner_text()
|
||||
self.logger.log(f"📋 옵션 {i+1}: '{option_text}'", level=logging.DEBUG)
|
||||
if target_option in option_text:
|
||||
await option_element.click()
|
||||
self.logger.log(f"📋 가격정책 옵션 클릭 성공: {target_option}", level=logging.DEBUG)
|
||||
break
|
||||
except Exception as e:
|
||||
continue
|
||||
else:
|
||||
self.logger.log(f"❌ 가격정책 옵션을 찾지 못함: {target_option}", level=logging.ERROR)
|
||||
return
|
||||
|
||||
# 변경 확인
|
||||
await asyncio.sleep(0.5)
|
||||
try:
|
||||
current_text = await current_selection.inner_text(timeout=5000)
|
||||
if target_option in current_text:
|
||||
self.logger.log(f"✅ 가격정책 변경 완료: {target_option}", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log(f"⚠️ 가격정책 변경 확인 실패. 현재: '{current_text}', 목표: '{target_option}'", level=logging.WARNING)
|
||||
except Exception as e:
|
||||
self.logger.log(f"⚠️ 가격정책 변경 확인 중 오류: {e}", level=logging.WARNING)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ 가격정책 드롭다운 변경 실패: {e}", level=logging.ERROR)
|
||||
|
||||
async def apply_smartstore_policy_checkbox(self, smartstore_policy: bool):
|
||||
"""스마트스토어정책 체크박스 변경"""
|
||||
try:
|
||||
self.logger.log(f"📋 스마트스토어정책 변경 시도: {'ON' if smartstore_policy else 'OFF'}", level=logging.DEBUG)
|
||||
|
||||
# 더 구체적인 선택자 사용: 스마트스토어 마켓 정책 체크박스만 선택
|
||||
try:
|
||||
# 방법 1: get_by_role을 사용해서 구체적으로 선택 (로그에서 확인된 정확한 이름 사용)
|
||||
smartstore_checkbox = self.page.get_by_role("checkbox", name="스마트스토어 마켓 정책 적용하기- 1")
|
||||
|
||||
# 현재 체크 상태 확인
|
||||
is_currently_checked = await smartstore_checkbox.is_checked()
|
||||
self.logger.log(f"📋 현재 스마트스토어정책 체크 상태: {'ON' if is_currently_checked else 'OFF'}", level=logging.DEBUG)
|
||||
|
||||
if is_currently_checked == smartstore_policy:
|
||||
self.logger.log(f"✅ 스마트스토어정책이 이미 {'ON' if smartstore_policy else 'OFF'}으로 설정되어 있음", level=logging.DEBUG)
|
||||
return
|
||||
|
||||
# 체크박스 상태 변경
|
||||
await smartstore_checkbox.click()
|
||||
self.logger.log(f"📋 스마트스토어정책 체크박스 {'ON' if smartstore_policy else 'OFF'} 클릭 완료", level=logging.DEBUG)
|
||||
|
||||
# 변경 확인
|
||||
await asyncio.sleep(0.5)
|
||||
final_checked = await smartstore_checkbox.is_checked()
|
||||
if final_checked == smartstore_policy:
|
||||
self.logger.log(f"✅ 스마트스토어정책 변경 완료: {'ON' if smartstore_policy else 'OFF'}", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log(f"⚠️ 스마트스토어정책 변경 확인 실패. 현재: {'ON' if final_checked else 'OFF'}, 목표: {'ON' if smartstore_policy else 'OFF'}", level=logging.WARNING)
|
||||
|
||||
except Exception as e1:
|
||||
self.logger.log(f"❌ 방법 1 실패, 방법 2 시도: {e1}", level=logging.DEBUG)
|
||||
|
||||
# 방법 2: 더 구체적인 선택자 사용
|
||||
try:
|
||||
# 모든 체크박스를 찾아서 텍스트로 구분
|
||||
all_checkboxes = await self.page.query_selector_all("div.ant-modal-content input[type='checkbox']")
|
||||
self.logger.log(f"📋 발견된 체크박스 수: {len(all_checkboxes)}", level=logging.DEBUG)
|
||||
|
||||
target_checkbox = None
|
||||
for i, checkbox in enumerate(all_checkboxes):
|
||||
try:
|
||||
# 체크박스 근처의 라벨 텍스트 확인
|
||||
parent_element = await checkbox.query_selector("..")
|
||||
if parent_element:
|
||||
parent_text = await parent_element.inner_text()
|
||||
self.logger.log(f"📋 체크박스 {i+1} 텍스트: '{parent_text}'", level=logging.DEBUG)
|
||||
|
||||
if "스마트스토어 마켓 정책" in parent_text or "마켓 정책" in parent_text:
|
||||
target_checkbox = checkbox
|
||||
self.logger.log(f"📋 스마트스토어정책 체크박스 발견: {i+1}번째", level=logging.DEBUG)
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if target_checkbox:
|
||||
is_currently_checked = await target_checkbox.is_checked()
|
||||
self.logger.log(f"📋 현재 스마트스토어정책 체크 상태: {'ON' if is_currently_checked else 'OFF'}", level=logging.DEBUG)
|
||||
|
||||
if is_currently_checked != smartstore_policy:
|
||||
await target_checkbox.click()
|
||||
self.logger.log(f"📋 스마트스토어정책 체크박스 {'ON' if smartstore_policy else 'OFF'} 클릭 완료", level=logging.DEBUG)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
final_checked = await target_checkbox.is_checked()
|
||||
if final_checked == smartstore_policy:
|
||||
self.logger.log(f"✅ 스마트스토어정책 변경 완료: {'ON' if smartstore_policy else 'OFF'}", level=logging.INFO)
|
||||
else:
|
||||
self.logger.log(f"⚠️ 스마트스토어정책 변경 확인 실패", level=logging.WARNING)
|
||||
else:
|
||||
self.logger.log("❌ 스마트스토어정책 체크박스를 찾지 못함", level=logging.ERROR)
|
||||
|
||||
except Exception as e2:
|
||||
self.logger.log(f"❌ 방법 2도 실패: {e2}", level=logging.ERROR)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ 스마트스토어정책 체크박스 변경 실패: {e}", level=logging.ERROR)
|
||||
|
||||
|
||||
#------------------------------------
|
||||
# 이미지 처리 관련 코드
|
||||
#------------------------------------
|
||||
|
|
@ -6373,12 +6836,41 @@ class ImageWorkerManager:
|
|||
self.logger.log(f"워커 재시작 실행: {cause}", level=logging.INFO)
|
||||
|
||||
try:
|
||||
# 프로세스 종료
|
||||
if self.proc.is_alive():
|
||||
self.proc.terminate()
|
||||
self.proc.join(timeout=3)
|
||||
if self.proc.is_alive():
|
||||
self.proc.kill()
|
||||
# 재시작 원인별 대기 시간 설정
|
||||
restart_delays = {
|
||||
"timeout": 5,
|
||||
"memory_threshold": 3,
|
||||
"memory_error": 10, # 메모리 에러는 더 긴 대기
|
||||
"error": 5,
|
||||
"periodic": 2
|
||||
}
|
||||
delay = restart_delays.get(cause, 3)
|
||||
|
||||
# 프로세스 안전하게 종료
|
||||
if hasattr(self, 'proc') and self.proc and self.proc.is_alive():
|
||||
try:
|
||||
self.logger.log(f"워커 프로세스 종료 시도 (PID: {self.proc.pid})", level=logging.DEBUG)
|
||||
self.proc.terminate()
|
||||
|
||||
# 정상 종료 대기 (더 긴 시간)
|
||||
if not self.proc.join(timeout=8):
|
||||
self.logger.log("정상 종료 실패 → 강제 종료", level=logging.WARNING)
|
||||
self.proc.kill()
|
||||
self.proc.join(timeout=2)
|
||||
|
||||
if self.proc.is_alive():
|
||||
self.logger.log("⚠️ 프로세스가 여전히 살아있음", level=logging.WARNING)
|
||||
else:
|
||||
self.logger.log("✅ 워커 프로세스 종료 완료", level=logging.DEBUG)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"프로세스 종료 중 오류: {e}", level=logging.WARNING)
|
||||
|
||||
# 안전한 대기 (메모리 안정화)
|
||||
if delay > 0:
|
||||
self.logger.log(f"메모리 안정화 대기: {delay}초", level=logging.DEBUG)
|
||||
import time
|
||||
time.sleep(delay)
|
||||
|
||||
# 강화된 메모리 정리
|
||||
self.logger.log("ImageWorker으로 인한 강화된 메모리 정리", level=logging.INFO)
|
||||
|
|
@ -6460,6 +6952,34 @@ class ImageWorkerManager:
|
|||
if old_count > 0:
|
||||
self.logger.log(f"🗑️ 재시작 시 {old_count}개 pending requests 정리 완료", level=logging.INFO)
|
||||
|
||||
# 재시작 원인별 안전 설정 적용
|
||||
updated_toggle_states = self.toggle_states.copy()
|
||||
|
||||
# 메모리/에러로 인한 재시작 시 더 안전한 설정 적용
|
||||
if cause in ["memory_error", "timeout", "error"]:
|
||||
self.logger.log(f"🔒 {cause}로 인한 재시작 → 안전 모드 설정 적용", level=logging.INFO)
|
||||
|
||||
# GPU 기능 비활성화
|
||||
updated_toggle_states['use_cuda'] = False
|
||||
updated_toggle_states['migan_use_cuda'] = False
|
||||
|
||||
# 모든 이미지 처리를 CPU 모드로 전환
|
||||
updated_toggle_states['optionIMGTrans_type'] = 'CPU'
|
||||
updated_toggle_states['detail_IMGTrans_type'] = 'CPU'
|
||||
updated_toggle_states['thumb_trans_type'] = 'CPU'
|
||||
|
||||
# 백그라운드 제거도 안전하게
|
||||
updated_toggle_states['thumb_nukki'] = False # 배경제거 비활성화
|
||||
|
||||
# 메모리 절약 설정
|
||||
updated_toggle_states['max_image_size'] = 800 # 이미지 크기 제한
|
||||
updated_toggle_states['enable_aggressive_memory_cleanup'] = True
|
||||
|
||||
self.logger.log("🔒 안전 모드: GPU 비활성화, CPU 모드, 배경제거 비활성화", level=logging.INFO)
|
||||
|
||||
# 업데이트된 toggle_states를 사용
|
||||
self.toggle_states = updated_toggle_states
|
||||
|
||||
# proc_args 업데이트
|
||||
log_path = self.proc_args[3] # log_path 위치 수정
|
||||
self.proc_args = (
|
||||
|
|
@ -6701,125 +7221,6 @@ class ImageWorkerManager:
|
|||
self.logger.log(f"❌ 응답 타임아웃: uid={uid[:8]}, 총 대기시간={elapsed:.1f}초", level=logging.WARNING)
|
||||
raise TimeoutError("image worker response timeout")
|
||||
|
||||
def start_UploadSelectedMarketsJob_task(self, upload_conditions=None):
|
||||
"""업로드 작업 시작 (MainUI에서 호출)"""
|
||||
try:
|
||||
# AsyncRunner를 통해 비동기 함수 실행
|
||||
if hasattr(self, 'async_runner') and self.async_runner:
|
||||
self.async_runner.run_async(self.ed_bulk_upload_selected_markets(upload_conditions))
|
||||
else:
|
||||
# 대안: QThread에서 실행
|
||||
asyncio.create_task(self.ed_bulk_upload_selected_markets(upload_conditions))
|
||||
|
||||
self.logger.log("업로드 작업 시작 요청 완료", level=logging.INFO)
|
||||
except Exception as e:
|
||||
self.logger.log(f"업로드 작업 시작 오류: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
async def apply_upload_conditions_to_page(self, price_policy: int, smartstore_policy: bool):
|
||||
"""업로드조건을 웹페이지에 실제로 적용"""
|
||||
try:
|
||||
self.logger.log(f"🔧 웹페이지에 업로드조건 적용 시작 - 가격정책: {price_policy}, 스마트스토어정책: {smartstore_policy}", level=logging.INFO)
|
||||
|
||||
# 1. 가격정책 드롭다운 변경
|
||||
await self.apply_price_policy_dropdown(price_policy)
|
||||
|
||||
# 2. 스마트스토어정책 체크박스 변경
|
||||
await self.apply_smartstore_policy_checkbox(smartstore_policy)
|
||||
|
||||
self.logger.log("✅ 웹페이지 업로드조건 적용 완료", level=logging.INFO)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ 웹페이지 업로드조건 적용 실패: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
async def apply_price_policy_dropdown(self, price_policy: int):
|
||||
"""가격정책 드롭다운 변경 (1: 최대 업로드 갯수 기준, 2: 최저 옵션가 기준, 3: 상품별 각각의 대표가격 설정)"""
|
||||
try:
|
||||
# 가격정책 옵션 텍스트 매핑
|
||||
policy_options = {
|
||||
1: "최대 업로드 갯수 기준",
|
||||
2: "최저 옵션가 기준",
|
||||
3: "상품별 각각의 대표 가격 설정 적용"
|
||||
}
|
||||
|
||||
target_option = policy_options.get(price_policy, policy_options[1])
|
||||
self.logger.log(f"📋 가격정책 변경 시도: {target_option}", level=logging.DEBUG)
|
||||
|
||||
# 현재 선택된 값 확인
|
||||
current_selection = self.page.locator("div.ant-modal-content .ant-select-selection-item")
|
||||
try:
|
||||
current_text = await current_selection.inner_text(timeout=5000)
|
||||
if target_option in current_text:
|
||||
self.logger.log(f"✅ 가격정책이 이미 '{target_option}'로 설정되어 있음", level=logging.DEBUG)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 드롭다운 클릭하여 열기
|
||||
dropdown_selector = "div.ant-modal-content .ant-select"
|
||||
dropdown = self.page.locator(dropdown_selector)
|
||||
await dropdown.click()
|
||||
self.logger.log("📋 가격정책 드롭다운 열기", level=logging.DEBUG)
|
||||
|
||||
# 드롭다운 옵션 리스트 대기
|
||||
await asyncio.sleep(0.5) # 드롭다운이 완전히 열릴 시간 확보
|
||||
|
||||
# 해당 옵션 클릭
|
||||
option_locator = self.page.get_by_text(target_option, exact=True).first
|
||||
await expect(option_locator).to_be_visible(timeout=5000)
|
||||
await option_locator.click()
|
||||
|
||||
self.logger.log(f"📋 가격정책 옵션 클릭: {target_option}", level=logging.DEBUG)
|
||||
|
||||
# 변경 확인
|
||||
await expect(current_selection).to_contain_text(target_option, timeout=5000)
|
||||
self.logger.log(f"✅ 가격정책 변경 완료: {target_option}", level=logging.INFO)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ 가격정책 드롭다운 변경 실패: {e}", level=logging.ERROR)
|
||||
|
||||
async def apply_smartstore_policy_checkbox(self, smartstore_policy: bool):
|
||||
"""스마트스토어정책 체크박스 변경"""
|
||||
try:
|
||||
self.logger.log(f"📋 스마트스토어정책 변경 시도: {'ON' if smartstore_policy else 'OFF'}", level=logging.DEBUG)
|
||||
|
||||
# 스마트스토어정책 체크박스 찾기
|
||||
checkbox_container = self.page.locator("div.ant-modal-content .ant-checkbox")
|
||||
checkbox_input = checkbox_container.locator("input.ant-checkbox-input")
|
||||
|
||||
# 현재 체크 상태 확인
|
||||
try:
|
||||
is_currently_checked = await checkbox_input.is_checked()
|
||||
if is_currently_checked == smartstore_policy:
|
||||
self.logger.log(f"✅ 스마트스토어정책이 이미 {'ON' if smartstore_policy else 'OFF'}으로 설정되어 있음", level=logging.DEBUG)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 체크박스 상태 변경
|
||||
if smartstore_policy:
|
||||
# 체크하기
|
||||
if not await checkbox_input.is_checked():
|
||||
await checkbox_container.click()
|
||||
self.logger.log("📋 스마트스토어정책 체크박스 ON", level=logging.DEBUG)
|
||||
else:
|
||||
# 체크 해제하기
|
||||
if await checkbox_input.is_checked():
|
||||
await checkbox_container.click()
|
||||
self.logger.log("📋 스마트스토어정책 체크박스 OFF", level=logging.DEBUG)
|
||||
|
||||
# 변경 확인
|
||||
if smartstore_policy:
|
||||
await expect(checkbox_input).to_be_checked(timeout=5000)
|
||||
await expect(checkbox_container).to_have_class(re.compile(r"ant-checkbox-checked"))
|
||||
else:
|
||||
await expect(checkbox_input).not_to_be_checked(timeout=5000)
|
||||
await expect(checkbox_container).not_to_have_class(re.compile(r"ant-checkbox-checked"))
|
||||
|
||||
self.logger.log(f"✅ 스마트스토어정책 변경 완료: {'ON' if smartstore_policy else 'OFF'}", level=logging.INFO)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"❌ 스마트스토어정책 체크박스 변경 실패: {e}", level=logging.ERROR)
|
||||
|
||||
def restart(self, cause="periodic"):
|
||||
"""cause: 'periodic' | 'error'"""
|
||||
"""현재 워커 프로세스를 종료하고 새로 띄운다"""
|
||||
|
|
|
|||
184
mainUI_SP.py
184
mainUI_SP.py
|
|
@ -90,9 +90,13 @@ class MAIN_GUI(QMainWindow):
|
|||
# 수동 로그인 QMessageBox 참조 저장
|
||||
self.manual_login_message_box = None
|
||||
|
||||
# 업로드조건 설정 변수들 초기화
|
||||
self.upload_price_policy = 1 # 1: 최대 업로드 갯수 기준, 2: 최저 옵션가 기준, 3: 상품별 각각의 대표가격 설정
|
||||
self.smartstore_market_policy = True # 스스 마켓정책적용 (기본값: True)
|
||||
# 업로드조건 설정 변수들 초기화 (settings에서 로드)
|
||||
self.upload_price_policy = self.settings_manager.get_value("upload_conditions/price_policy", 1) # 1: 최대 업로드 갯수 기준, 2: 최저 옵션가 기준, 3: 상품별 각각의 대표가격 설정
|
||||
self.smartstore_market_policy = self.settings_manager.get_value("upload_conditions/smartstore_market_policy", True) # 스스 마켓정책적용
|
||||
|
||||
# 로드된 업로드조건 설정값 로그 출력
|
||||
if hasattr(self, 'logger') and self.logger:
|
||||
self.logger.log(f"업로드조건 초기값 로드 - 가격정책: {self.upload_price_policy}, 스스정책: {self.smartstore_market_policy}", level=logging.DEBUG)
|
||||
|
||||
# 자동 스크롤 관련 변수 초기화
|
||||
self.scroll_timer = QTimer(self)
|
||||
|
|
@ -279,6 +283,18 @@ class MAIN_GUI(QMainWindow):
|
|||
self.browser_controller.image_worker_fatal_signal.connect(self.on_image_worker_fatal)
|
||||
self.browser_controller.premium_event_started.connect(self.on_premium_event_started)
|
||||
self.browser_controller.browser_login_error.connect(self.on_browser_login_error)
|
||||
|
||||
# 업로드 관련 시그널 연결 (항상 연결되도록)
|
||||
if hasattr(self.browser_controller, 'upload_progress_updated'):
|
||||
self.browser_controller.upload_progress_updated.connect(self.on_upload_progress_updated)
|
||||
if hasattr(self.browser_controller, 'upload_step_changed'):
|
||||
self.browser_controller.upload_step_changed.connect(self.on_upload_step_changed)
|
||||
if hasattr(self.browser_controller, 'upload_completed'):
|
||||
self.browser_controller.upload_completed.connect(self.on_upload_completed)
|
||||
if hasattr(self.browser_controller, 'upload_log_message'):
|
||||
self.browser_controller.upload_log_message.connect(self.on_upload_log_message)
|
||||
if hasattr(self.browser_controller, 'step_completed'):
|
||||
self.browser_controller.step_completed.connect(self.on_step_completed)
|
||||
self.browser_controller.browser_ad1_close_error.connect(self.on_browser_ad1_close_error)
|
||||
self.browser_controller.browser_ad2_close_error.connect(self.on_browser_ad2_close_error)
|
||||
self.browser_controller.browser_group_list_error.connect(self.on_browser_group_list_error)
|
||||
|
|
@ -3181,7 +3197,7 @@ class MAIN_GUI(QMainWindow):
|
|||
|
||||
# Percenty 버튼 텍스트 변경
|
||||
if self.is_register_product_mode:
|
||||
self.PercentyJob_button.setText('등록상품 \n 상품편집 시작')
|
||||
self.PercentyJob_button.setText('현재그룹\n 상품편집 시작')
|
||||
else:
|
||||
self.PercentyJob_button.setText('상품편집\n시작')
|
||||
|
||||
|
|
@ -3395,13 +3411,19 @@ class MAIN_GUI(QMainWindow):
|
|||
if reply == QMessageBox.StandardButton.Yes:
|
||||
|
||||
# 선택마켓 정보 확인
|
||||
full_info = self.biz_dbManager.get_biz_full_info()
|
||||
markets = full_info.get('markets', {})
|
||||
if not markets:
|
||||
work_combinations = self.biz_dbManager.get_biz_full_info()
|
||||
|
||||
# 모든 작업순서의 마켓을 합쳐서 확인
|
||||
all_markets = {}
|
||||
for work_combo in work_combinations:
|
||||
if isinstance(work_combo, dict) and 'markets' in work_combo:
|
||||
all_markets.update(work_combo['markets'])
|
||||
|
||||
if not all_markets:
|
||||
self.logger.log("선택된 마켓 정보가 없습니다. 먼저 사업자관리에서 마켓을 선택하세요.", level=logging.WARNING)
|
||||
return
|
||||
|
||||
self.logger.log(f"마켓정보전송 - {len(markets)}개 마켓", level=logging.INFO)
|
||||
self.logger.log(f"마켓정보전송 - {len(all_markets)}개 마켓", level=logging.INFO)
|
||||
|
||||
# 올바른 선택마켓 구조로 브라우저 컨트롤러에 전달
|
||||
legacy_info = self.biz_dbManager.get_biz_full_info_legacy()
|
||||
|
|
@ -3627,17 +3649,7 @@ class MAIN_GUI(QMainWindow):
|
|||
self.upload_progress_dialog.pause_requested.connect(self.on_upload_pause_requested)
|
||||
self.upload_progress_dialog.resume_requested.connect(self.on_upload_resume_requested)
|
||||
|
||||
# 브라우저 컨트롤러 시그널 연결 (진행상황 업데이트)
|
||||
if hasattr(self.browser_controller, 'upload_progress_updated'):
|
||||
self.browser_controller.upload_progress_updated.connect(self.on_upload_progress_updated)
|
||||
if hasattr(self.browser_controller, 'upload_step_changed'):
|
||||
self.browser_controller.upload_step_changed.connect(self.on_upload_step_changed)
|
||||
if hasattr(self.browser_controller, 'upload_completed'):
|
||||
self.browser_controller.upload_completed.connect(self.on_upload_completed)
|
||||
if hasattr(self.browser_controller, 'upload_log_message'):
|
||||
self.browser_controller.upload_log_message.connect(self.on_upload_log_message)
|
||||
if hasattr(self.browser_controller, 'step_completed'):
|
||||
self.browser_controller.step_completed.connect(self.on_step_completed)
|
||||
# 브라우저 컨트롤러 시그널은 이미 __init__에서 연결됨 (중복 연결 제거)
|
||||
|
||||
# 비모달로 다이얼로그 표시
|
||||
self.upload_progress_dialog.show()
|
||||
|
|
@ -3901,19 +3913,113 @@ class MAIN_GUI(QMainWindow):
|
|||
def on_upload_completed(self, success=True, message=""):
|
||||
"""업로드 완료 처리"""
|
||||
try:
|
||||
self.logger.log(f"📢 on_upload_completed 호출됨 - 성공: {success}, 메시지: {message}", level=logging.INFO)
|
||||
|
||||
# 진행률 다이얼로그가 있으면 업데이트
|
||||
if hasattr(self, 'upload_progress_dialog') and self.upload_progress_dialog:
|
||||
if success:
|
||||
self.upload_progress_dialog.complete_upload()
|
||||
self.upload_progress_dialog.add_log("모든 작업이 성공적으로 완료되었습니다!")
|
||||
completion_message = message if message else "모든 작업이 성공적으로 완료되었습니다!"
|
||||
self.upload_progress_dialog.add_log(completion_message)
|
||||
else:
|
||||
self.upload_progress_dialog.add_log(f"업로드 중 오류 발생: {message}")
|
||||
error_message = f"업로드 중 오류 발생: {message}"
|
||||
self.upload_progress_dialog.add_log(error_message)
|
||||
# 오류 발생 시에도 닫기 버튼 활성화
|
||||
self.upload_progress_dialog.pause_button.setEnabled(False)
|
||||
self.upload_progress_dialog.cancel_button.setEnabled(False)
|
||||
self.upload_progress_dialog.close_button.setEnabled(True)
|
||||
|
||||
# 항상 완료 알림 다이얼로그 표시 (UI 쓰레드에서 실행)
|
||||
if success:
|
||||
completion_message = message if message else "모든 작업이 성공적으로 완료되었습니다!"
|
||||
QTimer.singleShot(100, lambda: self.show_upload_completion_dialog(True, completion_message))
|
||||
else:
|
||||
error_message = f"업로드 중 오류 발생: {message}" if message else "업로드 중 오류가 발생했습니다."
|
||||
QTimer.singleShot(100, lambda: self.show_upload_completion_dialog(False, error_message))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"업로드 완료 처리 실패: {e}", level=logging.ERROR)
|
||||
|
||||
def show_upload_completion_dialog(self, success=True, message=""):
|
||||
"""업로드 완료/실패 알림 다이얼로그 표시"""
|
||||
try:
|
||||
self.logger.log(f"🔔 show_upload_completion_dialog 호출됨 - 성공: {success}, 메시지: {message}", level=logging.INFO)
|
||||
from PySide6.QtWidgets import QMessageBox
|
||||
from PySide6.QtCore import Qt
|
||||
|
||||
msg_box = QMessageBox(self)
|
||||
msg_box.setWindowTitle("업로드 완료")
|
||||
|
||||
if success:
|
||||
msg_box.setIcon(QMessageBox.Icon.Information)
|
||||
# 통계 정보 파싱 및 포맷팅
|
||||
if "총 성공:" in message and "총 실패:" in message:
|
||||
# 업로드 작업 완료 - 총 성공: 0건, 총 실패: 3건
|
||||
import re
|
||||
match = re.search(r'총 성공:\s*(\d+)건,\s*총 실패:\s*(\d+)건', message)
|
||||
if match:
|
||||
success_count = int(match.group(1))
|
||||
fail_count = int(match.group(2))
|
||||
total_count = success_count + fail_count
|
||||
|
||||
title = "📤 업로드 완료!"
|
||||
detailed_msg = f"""
|
||||
업로드 작업이 완료되었습니다.
|
||||
|
||||
📊 업로드 통계:
|
||||
• 총 처리 상품: {total_count}건
|
||||
• ✅ 성공: {success_count}건
|
||||
• ❌ 실패: {fail_count}건
|
||||
|
||||
성공률: {(success_count/total_count*100):.1f}% ({success_count}/{total_count})
|
||||
""".strip()
|
||||
msg_box.setText(title)
|
||||
msg_box.setDetailedText(detailed_msg)
|
||||
else:
|
||||
msg_box.setText("📤 업로드 완료!")
|
||||
msg_box.setDetailedText(message)
|
||||
else:
|
||||
msg_box.setText("📤 업로드 완료!")
|
||||
msg_box.setDetailedText(message)
|
||||
else:
|
||||
msg_box.setIcon(QMessageBox.Icon.Critical)
|
||||
msg_box.setText("❌ 업로드 실패")
|
||||
msg_box.setDetailedText(message)
|
||||
|
||||
# 버튼 설정
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
msg_box.setDefaultButton(QMessageBox.StandardButton.Ok)
|
||||
|
||||
# 다이얼로그 스타일 설정
|
||||
msg_box.setStyleSheet("""
|
||||
QMessageBox {
|
||||
background-color: white;
|
||||
font-size: 12px;
|
||||
}
|
||||
QMessageBox QLabel {
|
||||
color: #2c3e50;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
QPushButton:hover {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
""")
|
||||
|
||||
# 다이얼로그 표시
|
||||
msg_box.exec()
|
||||
|
||||
self.logger.log(f"업로드 완료 알림 표시 - 성공: {success}, 메시지: {message}", level=logging.INFO)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.log(f"업로드 완료 다이얼로그 표시 실패: {e}", level=logging.ERROR)
|
||||
|
||||
def on_market_change_button_clicked(self):
|
||||
"""마켓정보변경 버튼 클릭 핸들러"""
|
||||
|
|
@ -3938,13 +4044,19 @@ class MAIN_GUI(QMainWindow):
|
|||
return
|
||||
|
||||
# 선택마켓 정보 확인
|
||||
full_info = self.biz_dbManager.get_biz_full_info()
|
||||
markets = full_info.get('markets', {})
|
||||
if not markets:
|
||||
work_combinations = self.biz_dbManager.get_biz_full_info()
|
||||
|
||||
# 모든 작업순서의 마켓을 합쳐서 확인
|
||||
all_markets = {}
|
||||
for work_combo in work_combinations:
|
||||
if isinstance(work_combo, dict) and 'markets' in work_combo:
|
||||
all_markets.update(work_combo['markets'])
|
||||
|
||||
if not all_markets:
|
||||
self.logger.log("선택된 마켓 정보가 없습니다. 먼저 사업자관리에서 마켓을 선택하세요.", level=logging.WARNING)
|
||||
return
|
||||
|
||||
self.logger.log(f"마켓정보변경 시작 - {len(markets)}개 마켓", level=logging.INFO)
|
||||
self.logger.log(f"마켓정보변경 시작 - {len(all_markets)}개 마켓", level=logging.INFO)
|
||||
|
||||
# 올바른 선택마켓 구조로 브라우저 컨트롤러에 전달
|
||||
legacy_info = self.biz_dbManager.get_biz_full_info_legacy()
|
||||
|
|
@ -10237,25 +10349,33 @@ class MAIN_GUI(QMainWindow):
|
|||
def save_upload_conditions_settings(self):
|
||||
"""업로드조건 설정 저장"""
|
||||
try:
|
||||
self.settings_manager.set_value("upload_conditions/price_policy", self.upload_price_policy)
|
||||
self.settings_manager.set_value("upload_conditions/smartstore_market_policy", self.smartstore_market_policy)
|
||||
self.settings_manager.save_value("upload_conditions/price_policy", self.upload_price_policy)
|
||||
self.settings_manager.save_value("upload_conditions/smartstore_market_policy", self.smartstore_market_policy)
|
||||
self.logger.log("업로드조건 설정 저장 완료", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
self.logger.log(f"업로드조건 설정 저장 오류: {str(e)}", level=logging.ERROR)
|
||||
|
||||
def load_upload_conditions_settings(self):
|
||||
"""업로드조건 설정 불러오기"""
|
||||
"""업로드조건 설정 불러오기 (UI 반영)"""
|
||||
try:
|
||||
# 기본값 설정
|
||||
# settings에서 최신값 다시 로드 (다른 곳에서 변경되었을 수 있음)
|
||||
self.upload_price_policy = self.settings_manager.get_value("upload_conditions/price_policy", 1)
|
||||
self.smartstore_market_policy = self.settings_manager.get_value("upload_conditions/smartstore_market_policy", True)
|
||||
|
||||
# UI에 반영
|
||||
if hasattr(self, 'price_policy_combo'):
|
||||
# UI에 반영 (위젯이 생성된 후에만)
|
||||
if hasattr(self, 'price_policy_combo') and self.price_policy_combo is not None:
|
||||
# 시그널 차단 후 설정 (무한 루프 방지)
|
||||
self.price_policy_combo.blockSignals(True)
|
||||
self.price_policy_combo.setCurrentIndex(self.upload_price_policy - 1) # 0, 1, 2로 변환
|
||||
self.price_policy_combo.blockSignals(False)
|
||||
self.logger.log(f"가격정책 UI 반영: {self.upload_price_policy} -> index {self.upload_price_policy - 1}", level=logging.DEBUG)
|
||||
|
||||
if hasattr(self, 'smartstore_policy_toggle'):
|
||||
if hasattr(self, 'smartstore_policy_toggle') and self.smartstore_policy_toggle is not None:
|
||||
# 시그널 차단 후 설정 (무한 루프 방지)
|
||||
self.smartstore_policy_toggle.blockSignals(True)
|
||||
self.smartstore_policy_toggle.setChecked(self.smartstore_market_policy)
|
||||
self.smartstore_policy_toggle.blockSignals(False)
|
||||
self.logger.log(f"스마트스토어정책 UI 반영: {self.smartstore_market_policy}", level=logging.DEBUG)
|
||||
|
||||
self.logger.log(f"업로드조건 설정 불러오기 완료 - 가격정책: {self.upload_price_policy}, 스스정책: {self.smartstore_market_policy}", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
|
|
|
|||
|
|
@ -127,60 +127,118 @@ class BriaBackgroundRemovalModule:
|
|||
self.logger.log(self._init_error, level=logging.ERROR)
|
||||
return False
|
||||
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
|
||||
if self.logger:
|
||||
self.logger.log(f"BriaAI ONNX 모델 로딩 중: {self.local_model_path}", level=logging.INFO)
|
||||
|
||||
# 세션 옵션 설정 (성능 최적화)
|
||||
sess_options = ort.SessionOptions()
|
||||
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
||||
sess_options.enable_mem_pattern = True
|
||||
sess_options.enable_cpu_mem_arena = True
|
||||
|
||||
# DirectML 사용시 추가 설정
|
||||
if 'DmlExecutionProvider' in self.providers:
|
||||
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
# 단계별 폴백 시도
|
||||
fallback_providers = [
|
||||
(self.providers, "원본 providers"),
|
||||
([("CPUExecutionProvider", {})], "CPU 폴백")
|
||||
]
|
||||
|
||||
for attempt_providers, attempt_name in fallback_providers:
|
||||
try:
|
||||
import onnxruntime as ort
|
||||
|
||||
if self.logger:
|
||||
self.logger.log("DirectML 최적화 설정 적용", level=logging.DEBUG)
|
||||
self.logger.log(f"BriaAI ONNX 모델 로딩 시도 ({attempt_name}): {self.local_model_path}", level=logging.INFO)
|
||||
|
||||
# ONNX 세션 생성
|
||||
self._session = ort.InferenceSession(
|
||||
self.local_model_path,
|
||||
sess_options=sess_options,
|
||||
providers=self.providers
|
||||
)
|
||||
# 세션 옵션 설정 (안전성 우선)
|
||||
sess_options = ort.SessionOptions()
|
||||
|
||||
# CPU 모드에서는 더 보수적인 설정
|
||||
if attempt_providers == [("CPUExecutionProvider", {})]:
|
||||
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_DISABLE_ALL
|
||||
sess_options.enable_mem_pattern = False
|
||||
sess_options.enable_cpu_mem_arena = False
|
||||
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
if self.logger:
|
||||
self.logger.log("🔒 CPU 안전 모드: 모든 최적화 비활성화", level=logging.INFO)
|
||||
else:
|
||||
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_BASIC
|
||||
sess_options.enable_mem_pattern = True
|
||||
sess_options.enable_cpu_mem_arena = True
|
||||
|
||||
# DirectML 사용시 추가 설정
|
||||
if 'DmlExecutionProvider' in str(attempt_providers):
|
||||
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
|
||||
if self.logger:
|
||||
self.logger.log("DirectML 최적화 설정 적용", level=logging.DEBUG)
|
||||
|
||||
# 입출력 정보 가져오기
|
||||
inputs = self._session.get_inputs()
|
||||
outputs = self._session.get_outputs()
|
||||
|
||||
if not inputs or not outputs:
|
||||
raise RuntimeError("ONNX 모델의 입출력 정의를 찾을 수 없습니다")
|
||||
# ONNX 세션 생성 (타임아웃 설정)
|
||||
import signal
|
||||
|
||||
def timeout_handler(signum, frame):
|
||||
raise TimeoutError("모델 로딩 타임아웃")
|
||||
|
||||
# Windows에서는 signal.alarm이 작동하지 않으므로 threading 사용
|
||||
import threading
|
||||
import time
|
||||
|
||||
session_created = [False]
|
||||
session_result = [None]
|
||||
session_error = [None]
|
||||
|
||||
def create_session():
|
||||
try:
|
||||
session_result[0] = ort.InferenceSession(
|
||||
self.local_model_path,
|
||||
sess_options=sess_options,
|
||||
providers=attempt_providers
|
||||
)
|
||||
session_created[0] = True
|
||||
except Exception as e:
|
||||
session_error[0] = e
|
||||
|
||||
# 세션 생성을 별도 스레드에서 실행 (30초 타임아웃)
|
||||
session_thread = threading.Thread(target=create_session)
|
||||
session_thread.daemon = True
|
||||
session_thread.start()
|
||||
session_thread.join(timeout=30)
|
||||
|
||||
if not session_created[0]:
|
||||
if session_error[0]:
|
||||
raise session_error[0]
|
||||
else:
|
||||
raise TimeoutError("모델 로딩이 30초 내에 완료되지 않음")
|
||||
|
||||
self._session = session_result[0]
|
||||
|
||||
self._input_name = inputs[0].name
|
||||
self._output_name = outputs[0].name
|
||||
# 입출력 정보 가져오기
|
||||
inputs = self._session.get_inputs()
|
||||
outputs = self._session.get_outputs()
|
||||
|
||||
if not inputs or not outputs:
|
||||
raise RuntimeError("ONNX 모델의 입출력 정의를 찾을 수 없습니다")
|
||||
|
||||
# 실제 사용된 프로바이더 확인
|
||||
actual_providers = self._session.get_providers()
|
||||
|
||||
if self.logger:
|
||||
self.logger.log(
|
||||
f"✅ BriaAI ONNX 모델 로딩 완료 | "
|
||||
f"Providers: {actual_providers} | "
|
||||
f"Input: {self._input_name} | Output: {self._output_name}",
|
||||
level=logging.INFO
|
||||
)
|
||||
self._output_name = outputs[0].name
|
||||
|
||||
self._model_loaded = True
|
||||
return True
|
||||
# 실제 사용된 프로바이더 확인
|
||||
actual_providers = self._session.get_providers()
|
||||
|
||||
if self.logger:
|
||||
self.logger.log(
|
||||
f"✅ BriaAI ONNX 모델 로딩 완료 ({attempt_name}) | "
|
||||
f"Providers: {actual_providers} | "
|
||||
f"Input: {self._input_name} | Output: {self._output_name}",
|
||||
level=logging.INFO
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self._init_error = f"BriaAI ONNX 모델 로딩 실패: {e}"
|
||||
if self.logger:
|
||||
self.logger.log(self._init_error, level=logging.ERROR, exc_info=True)
|
||||
return False
|
||||
self._model_loaded = True
|
||||
self._providers_used = actual_providers
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"BriaAI ONNX 모델 로딩 실패 ({attempt_name}): {e}"
|
||||
if self.logger:
|
||||
self.logger.log(error_msg, level=logging.WARNING if attempt_name != "CPU 폴백" else logging.ERROR, exc_info=True)
|
||||
|
||||
# 마지막 시도가 아니면 계속 진행
|
||||
if attempt_name != "CPU 폴백":
|
||||
continue
|
||||
|
||||
# 모든 시도 실패
|
||||
self._init_error = "모든 provider에서 BriaAI ONNX 모델 로딩 실패"
|
||||
if self.logger:
|
||||
self.logger.log(self._init_error, level=logging.ERROR)
|
||||
return False
|
||||
|
||||
def _preprocess(self, image_bgr: np.ndarray) -> Tuple[np.ndarray, Tuple[int, int]]:
|
||||
"""BGR uint8 이미지를 모델 입력(NCHW float32, 정규화)로 변환 (허깅페이스 호환)"""
|
||||
|
|
|
|||
|
|
@ -83,20 +83,43 @@ class GPUManager:
|
|||
if not use_gpu_requested:
|
||||
self.logger.log("GPU 가속이 비활성화됨 (toggle_states['use_cuda'] = False)", level=logging.DEBUG)
|
||||
self.can_use_cuda = False
|
||||
self._set_safe_cpu_mode(toggle_states)
|
||||
return
|
||||
|
||||
# Windows 플랫폼 확인 (DirectML 필수)
|
||||
if platform.system() != "Windows":
|
||||
self.logger.log("DirectML은 Windows 전용입니다 - CPU 모드로 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
self._set_safe_cpu_mode(toggle_states)
|
||||
return
|
||||
|
||||
# DirectML 지원 확인
|
||||
directml_support = self._check_directml_support()
|
||||
# DirectML 지원 확인 (안전하게 시도)
|
||||
try:
|
||||
directml_support = self._check_directml_support()
|
||||
except Exception as e:
|
||||
self.logger.log(f"DirectML 확인 중 예외 발생: {e} - CPU 모드로 안전 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
self._set_safe_cpu_mode(toggle_states)
|
||||
return
|
||||
|
||||
if not directml_support:
|
||||
self.logger.log("DirectML 지원을 확인할 수 없음 - CPU 모드로 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
self._set_safe_cpu_mode(toggle_states)
|
||||
return
|
||||
|
||||
# 메모리 상태 확인 (안전장치)
|
||||
try:
|
||||
memory_ok = self._check_system_memory()
|
||||
if not memory_ok:
|
||||
self.logger.log("⚠️ 시스템 메모리 부족 감지 - 안전을 위해 CPU 모드로 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
self._set_safe_cpu_mode(toggle_states)
|
||||
return
|
||||
except Exception as e:
|
||||
self.logger.log(f"메모리 확인 실패: {e} - 안전을 위해 CPU 모드로 전환", level=logging.WARNING)
|
||||
self.can_use_cuda = False
|
||||
self._set_safe_cpu_mode(toggle_states)
|
||||
return
|
||||
|
||||
# 모든 검사 통과
|
||||
|
|
@ -113,6 +136,45 @@ class GPUManager:
|
|||
self.logger.log("📊 DirectML 가속 활성화: rembg, MIGAN, OCR 모든 모듈에서 GPU 사용", level=logging.DEBUG)
|
||||
self.logger.log("=== 🎯 DirectML GPU 상태 초기화 완료 🎯 ===", level=logging.DEBUG)
|
||||
|
||||
def _set_safe_cpu_mode(self, toggle_states: Dict[str, Any]) -> None:
|
||||
"""안전한 CPU 모드로 설정"""
|
||||
self.can_use_cuda = False
|
||||
self.directml_available = False
|
||||
|
||||
# 모든 GPU 관련 설정을 CPU 모드로 강제 변경
|
||||
gpu_related_keys = [
|
||||
'migan_use_cuda', 'use_cuda', 'optionIMGTrans_type',
|
||||
'detail_IMGTrans_type', 'thumb_trans_type'
|
||||
]
|
||||
|
||||
for key in gpu_related_keys:
|
||||
if key in toggle_states:
|
||||
if key.endswith('_type'):
|
||||
toggle_states[key] = 'CPU'
|
||||
else:
|
||||
toggle_states[key] = False
|
||||
|
||||
self.logger.log("🔒 안전한 CPU 모드로 모든 GPU 설정 강제 비활성화", level=logging.INFO)
|
||||
|
||||
def _check_system_memory(self) -> bool:
|
||||
"""시스템 메모리 상태 확인"""
|
||||
try:
|
||||
import psutil
|
||||
memory = psutil.virtual_memory()
|
||||
available_gb = memory.available / (1024**3)
|
||||
|
||||
self.logger.log(f"💾 시스템 메모리 - 사용가능: {available_gb:.1f}GB, 사용률: {memory.percent:.1f}%", level=logging.DEBUG)
|
||||
|
||||
# 사용 가능한 메모리가 2GB 미만이거나 사용률이 90% 이상이면 위험
|
||||
if available_gb < 2.0 or memory.percent > 90:
|
||||
self.logger.log(f"⚠️ 메모리 부족 위험: 사용가능 {available_gb:.1f}GB, 사용률 {memory.percent:.1f}%", level=logging.WARNING)
|
||||
return False
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.log(f"메모리 상태 확인 실패: {e}", level=logging.WARNING)
|
||||
return False # 확인 실패 시 안전하게 False 반환
|
||||
|
||||
def _detect_gpu_hardware(self) -> bool:
|
||||
"""GPU 하드웨어 감지"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -113,6 +113,11 @@ def worker_main(
|
|||
# ── ImageProcessor 초기화 및 Warm‑up ──────────────────────
|
||||
processor = None
|
||||
try:
|
||||
# 안전한 초기화를 위한 메모리 정리
|
||||
import gc
|
||||
gc.collect()
|
||||
|
||||
logger.info("🔧 ImageProcessor3 초기화 시작...")
|
||||
processor = ImageProcessor3(
|
||||
logger=logger,
|
||||
page=None,
|
||||
|
|
@ -123,19 +128,81 @@ def worker_main(
|
|||
papago_translator=None,
|
||||
)
|
||||
|
||||
# OCR 모델 로딩을 위한 더미 호출
|
||||
dummy_path = os.path.join(base_dir, "_imgproc_warmup.png")
|
||||
tmp = np.zeros((1, 1, 3), dtype=np.uint8)
|
||||
cv2.imwrite(dummy_path, tmp)
|
||||
processor.ocr_module.detect_text(dummy_path)
|
||||
try:
|
||||
os.remove(dummy_path)
|
||||
except Exception:
|
||||
pass
|
||||
# OCR 모델 안전한 Warm-up
|
||||
if processor and processor.ocr_module:
|
||||
try:
|
||||
logger.info("🔰 OCR 모듈 Warm-up 시작...")
|
||||
dummy_path = os.path.join(base_dir, "_imgproc_warmup.png")
|
||||
tmp = np.zeros((100, 100, 3), dtype=np.uint8) # 더 현실적인 크기
|
||||
cv2.imwrite(dummy_path, tmp)
|
||||
|
||||
# 타임아웃 설정으로 무한 대기 방지
|
||||
import threading
|
||||
import time
|
||||
|
||||
warmup_success = [False]
|
||||
warmup_error = [None]
|
||||
|
||||
def warmup_ocr():
|
||||
try:
|
||||
processor.ocr_module.detect_text(dummy_path)
|
||||
warmup_success[0] = True
|
||||
except Exception as e:
|
||||
warmup_error[0] = e
|
||||
|
||||
warmup_thread = threading.Thread(target=warmup_ocr)
|
||||
warmup_thread.daemon = True
|
||||
warmup_thread.start()
|
||||
warmup_thread.join(timeout=30) # 30초 타임아웃
|
||||
|
||||
if warmup_success[0]:
|
||||
logger.info("✅ OCR 모듈 Warm-up 성공")
|
||||
elif warmup_error[0]:
|
||||
logger.warning(f"⚠️ OCR 모듈 Warm-up 실패: {warmup_error[0]}")
|
||||
else:
|
||||
logger.warning("⚠️ OCR 모듈 Warm-up 타임아웃")
|
||||
|
||||
try:
|
||||
os.remove(dummy_path)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"OCR Warm-up 실패: {e}")
|
||||
else:
|
||||
logger.warning("OCR 모듈이 초기화되지 않아 Warm-up 건너뜀")
|
||||
|
||||
logger.info("🔰 ImageProcessor Warm‑up 완료")
|
||||
except Exception:
|
||||
logger.error("ImageProcessor 초기화 실패", exc_info=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ImageProcessor 초기화 실패: {e}", exc_info=True)
|
||||
|
||||
# 초기화 실패 시에도 기본적인 처리가 가능하도록 최소한의 processor 생성 시도
|
||||
try:
|
||||
logger.info("🔄 안전 모드로 재초기화 시도...")
|
||||
|
||||
# GPU 설정을 CPU로 강제 변경
|
||||
safe_toggle_states = toggle_states.copy()
|
||||
safe_toggle_states['use_cuda'] = False
|
||||
safe_toggle_states['optionIMGTrans_type'] = 'CPU'
|
||||
safe_toggle_states['detail_IMGTrans_type'] = 'CPU'
|
||||
safe_toggle_states['thumb_trans_type'] = 'CPU'
|
||||
safe_toggle_states['migan_use_cuda'] = False
|
||||
|
||||
processor = ImageProcessor3(
|
||||
logger=logger,
|
||||
page=None,
|
||||
toggle_states=safe_toggle_states,
|
||||
unwanted_words=unwanted_words,
|
||||
authenticated_by_admin=authenticated_by_admin,
|
||||
base_dir=base_dir,
|
||||
papago_translator=None,
|
||||
)
|
||||
logger.info("✅ 안전 모드로 ImageProcessor 초기화 성공")
|
||||
|
||||
except Exception as e2:
|
||||
logger.error(f"안전 모드 초기화도 실패: {e2}", exc_info=True)
|
||||
processor = None
|
||||
|
||||
# ── READY(OK) 신호 전송 ──────────────────────────────────
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -312,31 +312,87 @@ class ONNXOCRModule:
|
|||
os.environ['MKL_NUM_THREADS'] = '1'
|
||||
os.environ['NUMEXPR_NUM_THREADS'] = '1'
|
||||
|
||||
# ONNX 시스템 초기화
|
||||
# ONNX 시스템 초기화 (안전한 폴백 로직)
|
||||
self.text_system = None
|
||||
self._initialize_onnx_system()
|
||||
initialization_attempts = []
|
||||
|
||||
if self.text_system is None:
|
||||
# DirectML 실패 시 CPU 모드로 재시도
|
||||
if self.use_gpu:
|
||||
# 1차 시도: 원본 설정으로 초기화
|
||||
try:
|
||||
if self.logger:
|
||||
initial_mode = "DirectML" if self.use_gpu else "CPU"
|
||||
self.logger.log(f"🚀 ONNX TextSystem 초기화 시작 ({initial_mode} 모드)", level=logging.INFO)
|
||||
|
||||
self._initialize_onnx_system()
|
||||
|
||||
if self.text_system is not None:
|
||||
gpu_status = "DirectML" if self.use_gpu else "CPU"
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ ONNX TextSystem 초기화 완료 ({gpu_status} + {self.model_type.upper()} 모델)", level=logging.INFO)
|
||||
self.logger.log(f"✅ ONNX OCR 모듈 초기화 성공 ({gpu_status} 모드)", level=logging.INFO)
|
||||
return
|
||||
else:
|
||||
initialization_attempts.append("원본 설정 실패")
|
||||
|
||||
except Exception as e:
|
||||
initialization_attempts.append(f"원본 설정 예외: {e}")
|
||||
if self.logger:
|
||||
self.logger.log(f"ONNX 초기화 1차 시도 실패: {e}", level=logging.WARNING)
|
||||
|
||||
# 2차 시도: DirectML -> CPU 폴백
|
||||
if self.use_gpu:
|
||||
try:
|
||||
if self.logger:
|
||||
self.logger.log("🔄 DirectML 실패로 CPU 모드로 재시도 중...", level=logging.WARNING)
|
||||
|
||||
self.use_gpu = False
|
||||
self._initialize_onnx_system() # CPU 모드로 재시도
|
||||
|
||||
if self.text_system is None:
|
||||
if self.text_system is not None:
|
||||
if self.logger:
|
||||
self.logger.log("❌ CPU 모드도 실패: ONNX TextSystem 초기화 실패", level=logging.ERROR)
|
||||
raise Exception("ONNX TextSystem 초기화 완전 실패")
|
||||
else:
|
||||
self.logger.log("✅ CPU 폴백 모드로 ONNX TextSystem 초기화 성공", level=logging.INFO)
|
||||
self.logger.log(f"✅ ONNX OCR 모듈 초기화 성공 (CPU 폴백 모드)", level=logging.INFO)
|
||||
return
|
||||
else:
|
||||
initialization_attempts.append("CPU 폴백 실패")
|
||||
|
||||
except Exception as e:
|
||||
initialization_attempts.append(f"CPU 폴백 예외: {e}")
|
||||
if self.logger:
|
||||
self.logger.log("❌ ONNX TextSystem 초기화 실패", level=logging.ERROR)
|
||||
raise Exception("ONNX TextSystem 초기화 실패")
|
||||
else:
|
||||
gpu_status = "DirectML" if self.use_gpu else "CPU"
|
||||
if self.logger:
|
||||
self.logger.log(f"✅ ONNX OCR 모듈 초기화 성공 ({gpu_status} 모드)", level=logging.INFO)
|
||||
self.logger.log(f"CPU 폴백 시도도 실패: {e}", level=logging.WARNING)
|
||||
|
||||
# 3차 시도: 더 안전한 모델로 재시도 (simp 모델)
|
||||
if self.model_type != 'simp':
|
||||
try:
|
||||
if self.logger:
|
||||
self.logger.log("🔄 더 안전한 SIMP 모델로 재시도 중...", level=logging.WARNING)
|
||||
|
||||
original_model_type = self.model_type
|
||||
self.model_type = 'simp' # 가장 안정적인 모델로 변경
|
||||
self.use_gpu = False # CPU 모드 강제
|
||||
self._initialize_onnx_system()
|
||||
|
||||
if self.text_system is not None:
|
||||
if self.logger:
|
||||
self.logger.log("✅ SIMP 모델 + CPU 모드로 ONNX TextSystem 초기화 성공", level=logging.INFO)
|
||||
self.logger.log(f"✅ ONNX OCR 모듈 초기화 성공 (SIMP + CPU 안전 모드)", level=logging.INFO)
|
||||
return
|
||||
else:
|
||||
self.model_type = original_model_type # 원복
|
||||
initialization_attempts.append("SIMP 모델 폴백 실패")
|
||||
|
||||
except Exception as e:
|
||||
initialization_attempts.append(f"SIMP 모델 폴백 예외: {e}")
|
||||
if self.logger:
|
||||
self.logger.log(f"SIMP 모델 폴백도 실패: {e}", level=logging.ERROR)
|
||||
|
||||
# 모든 시도 실패
|
||||
error_summary = " | ".join(initialization_attempts)
|
||||
final_error = f"ONNX TextSystem 모든 초기화 시도 실패: {error_summary}"
|
||||
|
||||
if self.logger:
|
||||
self.logger.log(f"❌ {final_error}", level=logging.ERROR)
|
||||
|
||||
raise Exception(final_error)
|
||||
|
||||
def _determine_model_type(self):
|
||||
"""toggle_states에서 모델 타입을 결정합니다."""
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -19,7 +19,6 @@
|
|||
### 패치 1 기능변경
|
||||
- 상세페이지 옵션명 입력시 넘버링 일치 적용
|
||||
- 워터마크 메뉴 제거
|
||||
- 업로드알바생 내부로 통합
|
||||
- 그룹리스트 탐색방식 개선
|
||||
- 옵션 넘버링 선택 제공
|
||||
- 옵션정보를 상세페이지에 표시할때 옵션타입2,3에 대한 처리 추가
|
||||
|
|
@ -40,17 +39,18 @@
|
|||
포함방식 : '샤넬'과 '샤넬가방' 모두 삭제
|
||||
- 중지버튼 삭제
|
||||
- 첫번째 옵션이미지의 대표썸네일 설정 기능 추가
|
||||
- 등록상품모드 추가
|
||||
- 업로드알바생(등록상품모드) 내부로 통합
|
||||
- 등록상품모드에서 등급별 편집가능한 범위
|
||||
Basic - 상품명 편집
|
||||
Premium - 상품명 편집 + 가격 편집
|
||||
VIP - 상품명 편집 + 썸네일 편집 + 가격 편집 + 옵션편집 + 상세페이지 편집
|
||||
VIP - 상품명 편집 + 썸네일 편집 + 가격 편집 + 사업자api변경(3개 관리)
|
||||
|
||||
상품명 : 기존 키고정 여부와 셔플 or 생성을 모두 적용 가능
|
||||
썸네일 : 동일이미지 판단 해시우회를 위해 1.좌우반전, 2.미세한각도조정(1~2도), 3.채도/밝기 미세조정, 4.스케일조정, 5.가우시안노이즈조정, 6.미세블러적용 7.배경미세텍스쳐 8.배경색랜덤변경 등으로 사용자눈에는 차이가 없으나 해시값의 변화를 불러오는 내용 적용.
|
||||
옵션 : 다양한 넘버링 랜덤 변경적용.
|
||||
썸네일 : 동일이미지 판단 해시우회를 위해 1.좌우반전, 2.미세한각도조정(1~2도), 3.채도/밝기 미세조정, 4.스케일조정,
|
||||
5.노이즈조정, 6.미세블러적용 7.배경미세텍스쳐 등으로 사용자 눈에는 차이가 없으나 해시값의 변화를 불러오는 내용 적용.
|
||||
가격 : -3% ~ +3% 범위로 퍼센트마진을 조정하여 최종값 미세조정
|
||||
상세페이지 : 선택마켓에서 스마트스토어로 지정된 사업자의 이름을 가져와 상세페이지의 윗줄에 환영문구룰 추가합니다. 없을경우 랜덤 환영문구가 추가됩니다.
|
||||
|
||||
- 암튼 더더 많이 바뀜(누락된 잠수패치가 있을수 있습니다.)
|
||||
|
||||
# 3.12.0 마이너 업데이트 로그
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue