브라우저 컨트롤러 개선 및 업로드 조건 설정 로직 수정: 업로드 조건 설정 변수를 초기화하고, 관련 시그널을 연결하여 UI와의 연동을 강화하였습니다. 또한, ONNX 모델 로딩 및 GPU 사용 관련 안전성 개선을 위한 폴백 로직을 추가하였습니다. 데이터베이스 파일이 업데이트되었습니다.

This commit is contained in:
9700X_PC 2025-09-28 00:26:53 +09:00
parent fc0d7050e7
commit 71947fca83
11 changed files with 1781 additions and 261 deletions

View File

@ -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;

View File

@ -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;

Binary file not shown.

View File

@ -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'"""
"""현재 워커 프로세스를 종료하고 새로 띄운다"""

View File

@ -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:

View File

@ -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, 정규화)로 변환 (허깅페이스 호환)"""

View File

@ -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:

View File

@ -113,6 +113,11 @@ def worker_main(
# ── ImageProcessor 초기화 및 Warmup ──────────────────────
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 Warmup 완료")
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:

View File

@ -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에서 모델 타입을 결정합니다."""

View File

@ -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 마이너 업데이트 로그