Compare commits
No commits in common. "master" and "new_test" have entirely different histories.
|
|
@ -7,3 +7,5 @@ src/browsers/cache/
|
|||
src/browsers/user_data/
|
||||
login/__pycache__/
|
||||
src/__pycache__/
|
||||
build/
|
||||
dist/
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
def build():
|
||||
# PyInstaller 명령어 옵션
|
||||
command = [
|
||||
"pyinstaller",
|
||||
"--onefile", # 단일 실행 파일 생성
|
||||
"--windowed", # 콘솔 창 없이 GUI 모드로 실행
|
||||
"--icon=icon.ico", # 아이콘 포함 (icon.ico 파일이 존재해야 함)
|
||||
"--add-data", "src/browsers;browsers", # src/browsers 폴더를 실행파일 내의 browsers 폴더로 포함 (Windows: 세미콜론, Mac/Linux: 콜론)
|
||||
"--hidden-import=playwright", # 자동 탐지되지 않는 의존성 포함
|
||||
"--hidden-import=PySide6",
|
||||
"--clean", # 이전 빌드 캐시 삭제
|
||||
"--noconfirm", # 기존 빌드 덮어쓰기 확인 없이 진행
|
||||
"main.py" # 진입점 스크립트
|
||||
]
|
||||
|
||||
|
||||
# 작업 디렉토리를 스크립트 파일이 있는 폴더로 변경 (선택사항)
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
print("PyInstaller 빌드를 시작합니다...")
|
||||
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
|
||||
print("===== stdout =====")
|
||||
print(result.stdout)
|
||||
print("===== stderr =====")
|
||||
print(result.stderr)
|
||||
|
||||
if result.returncode != 0:
|
||||
print("빌드 중 오류가 발생했습니다.")
|
||||
sys.exit(result.returncode)
|
||||
else:
|
||||
print("빌드가 성공적으로 완료되었습니다.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
## Why this file is included
|
||||
|
||||
This program has been frozen with cx_Freeze. The freezing process
|
||||
resulted in certain components from the cx_Freeze software being included
|
||||
in the frozen application, in particular bootstrap code for launching
|
||||
the frozen python script. The cx_Freeze software is subject to the
|
||||
license set out below.
|
||||
|
||||
# Licensing
|
||||
|
||||
- Copyright © 2020-2025, Marcelo Duarte.
|
||||
- Copyright © 2007-2019, Anthony Tuininga.
|
||||
- Copyright © 2001-2006, Computronix (Canada) Ltd., Edmonton, Alberta,
|
||||
Canada.
|
||||
- All rights reserved.
|
||||
|
||||
NOTE: This license is derived from the Python Software Foundation
|
||||
License which can be found at
|
||||
<https://docs.python.org/3/license.html#psf-license-agreement-for-python-release>
|
||||
|
||||
## License for cx_Freeze
|
||||
|
||||
1. This LICENSE AGREEMENT is between the copyright holders and the
|
||||
Individual or Organization ("Licensee") accessing and otherwise
|
||||
using cx_Freeze software in source or binary form and its associated
|
||||
documentation.
|
||||
2. Subject to the terms and conditions of this License Agreement, the
|
||||
copyright holders hereby grant Licensee a nonexclusive,
|
||||
royalty-free, world-wide license to reproduce, analyze, test,
|
||||
perform and/or display publicly, prepare derivative works,
|
||||
distribute, and otherwise use cx_Freeze alone or in any derivative
|
||||
version, provided, however, that this License Agreement and this
|
||||
notice of copyright are retained in cx_Freeze alone or in any
|
||||
derivative version prepared by Licensee.
|
||||
3. In the event Licensee prepares a derivative work that is based on or
|
||||
incorporates cx_Freeze or any part thereof, and wants to make the
|
||||
derivative work available to others as provided herein, then
|
||||
Licensee hereby agrees to include in any such work a brief summary
|
||||
of the changes made to cx_Freeze.
|
||||
4. The copyright holders are making cx_Freeze available to Licensee on
|
||||
an "AS IS" basis. THE COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR
|
||||
WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT
|
||||
LIMITATION, THE COPYRIGHT HOLDERS MAKE NO AND DISCLAIM ANY
|
||||
REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY
|
||||
PARTICULAR PURPOSE OR THAT THE USE OF CX_FREEZE WILL NOT INFRINGE
|
||||
ANY THIRD PARTY RIGHTS.
|
||||
5. THE COPYRIGHT HOLDERS SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER
|
||||
USERS OF CX_FREEZE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL
|
||||
DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE
|
||||
USING CX_FREEZE, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY THEREOF.
|
||||
6. This License Agreement will automatically terminate upon a material
|
||||
breach of its terms and conditions.
|
||||
7. Nothing in this License Agreement shall be deemed to create any
|
||||
relationship of agency, partnership, or joint venture between the
|
||||
copyright holders and Licensee. This License Agreement does not
|
||||
grant permission to use copyright holder's trademarks or trade name
|
||||
in a trademark sense to endorse or promote products or services of
|
||||
Licensee, or any third party.
|
||||
8. By copying, installing or otherwise using cx_Freeze, Licensee agrees
|
||||
to be bound by the terms and conditions of this License Agreement.
|
||||
|
||||
Computronix® is a registered trademark of Computronix (Canada) Ltd.
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
|
|
@ -1,59 +0,0 @@
|
|||
[2025-03-25 21:08:36,340] [MainThread] [DEBUG] [sp_manager.py:update_client_with_token:36] Client updated with JWT token
|
||||
[2025-03-25 21:08:36,340] [MainThread] [DEBUG] [sp_manager.py:login:121] 로그인 성공
|
||||
[2025-03-25 21:08:36,340] [MainThread] [DEBUG] [sp_manager.py:login:122] response : user=User(id='909d2ef8-7053-4006-ab40-49eb49f20383', app_metadata={'provider': 'email', 'providers': ['email']}, user_metadata={'email': 'leensoo1nt@gmail.com', 'email_verified': False, 'phone_verified': False, 'sub': '909d2ef8-7053-4006-ab40-49eb49f20383'}, aud='authenticated', confirmation_sent_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 784925, tzinfo=TzInfo(UTC)), recovery_sent_at=None, email_change_sent_at=None, new_email=None, new_phone=None, invited_at=None, action_link=None, email='leensoo1nt@gmail.com', phone='', created_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 774578, tzinfo=TzInfo(UTC)), confirmed_at=datetime.datetime(2025, 2, 18, 15, 16, 28, 322591, tzinfo=TzInfo(UTC)), email_confirmed_at=datetime.datetime(2025, 2, 18, 15, 16, 28, 322591, tzinfo=TzInfo(UTC)), phone_confirmed_at=None, last_sign_in_at=datetime.datetime(2025, 3, 25, 12, 10, 42, 925387, tzinfo=TzInfo(UTC)), role='authenticated', updated_at=datetime.datetime(2025, 3, 25, 12, 10, 42, 927200, tzinfo=TzInfo(UTC)), identities=[UserIdentity(id='909d2ef8-7053-4006-ab40-49eb49f20383', identity_id='02512f8f-12ee-4925-87f1-7e83f4dca8ed', user_id='909d2ef8-7053-4006-ab40-49eb49f20383', identity_data={'email': 'leensoo1nt@gmail.com', 'email_verified': False, 'phone_verified': False, 'sub': '909d2ef8-7053-4006-ab40-49eb49f20383'}, provider='email', created_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 781602, tzinfo=TzInfo(UTC)), last_sign_in_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 781558, tzinfo=TzInfo(UTC)), updated_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 781602, tzinfo=TzInfo(UTC)))], is_anonymous=False, factors=None) session=Session(provider_token=None, provider_refresh_token=None, access_token='eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5MDlkMmVmOC03MDUzLTQwMDYtYWI0MC00OWViNDlmMjAzODMiLCJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNzQyOTA4MjQyLCJpYXQiOjE3NDI5MDQ2NDIsImVtYWlsIjoibGVlbnNvbzFudEBnbWFpbC5jb20iLCJwaG9uZSI6IiIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6ImVtYWlsIiwicHJvdmlkZXJzIjpbImVtYWlsIl19LCJ1c2VyX21ldGFkYXRhIjp7ImVtYWlsIjoibGVlbnNvbzFudEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsInBob25lX3ZlcmlmaWVkIjpmYWxzZSwic3ViIjoiOTA5ZDJlZjgtNzA1My00MDA2LWFiNDAtNDllYjQ5ZjIwMzgzIn0sInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiYWFsIjoiYWFsMSIsImFtciI6W3sibWV0aG9kIjoicGFzc3dvcmQiLCJ0aW1lc3RhbXAiOjE3NDI5MDQ2NDJ9XSwic2Vzc2lvbl9pZCI6ImE5ZjU4Zjk1LTRlYTYtNDkzOC1iNWUxLTRkOTFhYjNjNGQ0MiIsImlzX2Fub255bW91cyI6ZmFsc2V9.V28ngwVftDVEFnezjT1FWy8gj1flJJpZfz8CePZY-dE', refresh_token='PR7Eo-oZTceqiroZhuVP5Q', expires_in=3600, expires_at=1742908242, token_type='bearer', user=User(id='909d2ef8-7053-4006-ab40-49eb49f20383', app_metadata={'provider': 'email', 'providers': ['email']}, user_metadata={'email': 'leensoo1nt@gmail.com', 'email_verified': False, 'phone_verified': False, 'sub': '909d2ef8-7053-4006-ab40-49eb49f20383'}, aud='authenticated', confirmation_sent_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 784925, tzinfo=TzInfo(UTC)), recovery_sent_at=None, email_change_sent_at=None, new_email=None, new_phone=None, invited_at=None, action_link=None, email='leensoo1nt@gmail.com', phone='', created_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 774578, tzinfo=TzInfo(UTC)), confirmed_at=datetime.datetime(2025, 2, 18, 15, 16, 28, 322591, tzinfo=TzInfo(UTC)), email_confirmed_at=datetime.datetime(2025, 2, 18, 15, 16, 28, 322591, tzinfo=TzInfo(UTC)), phone_confirmed_at=None, last_sign_in_at=datetime.datetime(2025, 3, 25, 12, 10, 42, 925387, tzinfo=TzInfo(UTC)), role='authenticated', updated_at=datetime.datetime(2025, 3, 25, 12, 10, 42, 927200, tzinfo=TzInfo(UTC)), identities=[UserIdentity(id='909d2ef8-7053-4006-ab40-49eb49f20383', identity_id='02512f8f-12ee-4925-87f1-7e83f4dca8ed', user_id='909d2ef8-7053-4006-ab40-49eb49f20383', identity_data={'email': 'leensoo1nt@gmail.com', 'email_verified': False, 'phone_verified': False, 'sub': '909d2ef8-7053-4006-ab40-49eb49f20383'}, provider='email', created_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 781602, tzinfo=TzInfo(UTC)), last_sign_in_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 781558, tzinfo=TzInfo(UTC)), updated_at=datetime.datetime(2025, 2, 18, 15, 15, 24, 781602, tzinfo=TzInfo(UTC)))], is_anonymous=False, factors=None))
|
||||
[2025-03-25 21:08:36,340] [MainThread] [DEBUG] [sp_manager.py:login:123] user_info : {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-25 21:08:36,559] [MainThread] [WARNING] [sp_manager.py:get_full_user_info:168] user_resp : data=[{'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'nickname': '리앤수', 'created_at': '2025-02-19T00:15:24.774065', 'updated_at': '2025-02-19T00:15:24.774065', 'membership_level': 'premium', 'last_login': '2025-03-25T12:07:05.488341', 'privacy_consent': True, 'privacy_consent_date': '2025-02-19T00:13:56.357521+09:00', 'license_consent': True, 'license_consent_date': '2025-02-19T00:13:56.357521+09:00', 'username': '한효상', 'email': 'leensoo1nt@gmail.com', 'email_confirmed_at': '2025-02-19T00:16:28.322591+09:00', 'payment_info': None, 'payment_period_end': None, 'requested_api_call_count': None, 'current_concurrent': 24, 'role': None, 'authenticated_by_admin': True, 'price_settings': None, 'category_settings': None}] count=None
|
||||
[2025-03-25 21:08:36,574] [MainThread] [DEBUG] [login_dialog.py:handle_login:119] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'nickname': '리앤수', 'created_at': '2025-02-19T00:15:24.774065', 'updated_at': '2025-02-19T00:15:24.774065', 'membership_level': 'premium', 'last_login': '2025-03-25T12:07:05.488341', 'privacy_consent': True, 'privacy_consent_date': '2025-02-19T00:13:56.357521+09:00', 'license_consent': True, 'license_consent_date': '2025-02-19T00:13:56.357521+09:00', 'username': '한효상', 'email': 'leensoo1nt@gmail.com', 'email_confirmed_at': '2025-02-19T00:16:28.322591+09:00', 'payment_info': None, 'payment_period_end': None, 'requested_api_call_count': None, 'current_concurrent': 24, 'role': None, 'authenticated_by_admin': True, 'price_settings': None, 'category_settings': None, 'membership_level_data': {'level': 'premium', 'api_call_limit': 3000, 'max_rows_per_query': 100, 'accessible_tables': ['common_banned_words', 'user_roles'], 'created_at': '2025-01-23T23:55:48.673088', 'concurrent_login_limit': 4, 'updated_at': None}}
|
||||
[2025-03-25 21:08:36,754] [MainThread] [DEBUG] [sp_manager.py:check_membership_validity:563] check_membership_validity - period_end_str : None
|
||||
[2025-03-25 21:08:36,759] [MainThread] [DEBUG] [databaseManager.py:create_table:22] 데이터베이스 테이블 생성 완료
|
||||
[2025-03-25 21:08:36,765] [MainThread] [DEBUG] [databaseManager.py:fetch_all:41] 데이터 로드 완료
|
||||
[2025-03-25 21:08:38,176] [MainThread] [INFO] [main_window.py:on_login_button_clicked:161] 로그인 버튼 클릭 - QR 로그인 요청
|
||||
[2025-03-25 21:08:38,182] [Dummy-2] [DEBUG] [jjim_runner.py:start_browser:113] 작업 디렉토리 변경: D:\py\jjim2\build\exe.win-amd64-3.11
|
||||
[2025-03-25 21:08:38,183] [Dummy-2] [DEBUG] [jjim_runner.py:start_browser:121] 브라우저 실행 파일이 없습니다: D:\py\jjim2\build\exe.win-amd64-3.11\browsers\chromium-1140\chrome-win\chrome.exe
|
||||
[2025-03-25 21:08:38,183] [Dummy-2] [ERROR] [jjim_runner.py:start_browser:180] 브라우저 초기화 오류: 브라우저 실행 파일이 없습니다: D:\py\jjim2\build\exe.win-amd64-3.11\browsers\chromium-1140\chrome-win\chrome.exe
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\jjim2\src\jjim_runner.py", line 122, in start_browser
|
||||
raise FileNotFoundError(f"브라우저 실행 파일이 없습니다: {browser_path}")
|
||||
FileNotFoundError: 브라우저 실행 파일이 없습니다: D:\py\jjim2\build\exe.win-amd64-3.11\browsers\chromium-1140\chrome-win\chrome.exe
|
||||
|
||||
[2025-03-25 21:08:43,929] [MainThread] [INFO] [main_window.py:on_login_button_clicked:161] 로그인 버튼 클릭 - QR 로그인 요청
|
||||
[2025-03-25 21:08:43,931] [Dummy-3] [DEBUG] [jjim_runner.py:start_browser:113] 작업 디렉토리 변경: D:\py\jjim2\build\exe.win-amd64-3.11\lib
|
||||
[2025-03-25 21:08:43,933] [Dummy-3] [DEBUG] [jjim_runner.py:start_browser:127] D:\py\jjim2\build\exe.win-amd64-3.11\lib\browsers\user_data 디렉토리가 생성되었습니다.
|
||||
[2025-03-25 21:08:43,934] [Dummy-3] [DEBUG] [jjim_runner.py:start_browser:132] D:\py\jjim2\build\exe.win-amd64-3.11\lib\browsers\cache 디렉토리가 생성되었습니다.
|
||||
[2025-03-25 21:08:43,936] [Dummy-3] [ERROR] [jjim_runner.py:start_browser:180] 브라우저 초기화 오류: [WinError 2] 지정된 파일을 찾을 수 없습니다
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\jjim2\src\jjim_runner.py", line 143, in start_browser
|
||||
self.playwright = await async_playwright().start()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\py\jjim2\Lib\site-packages\playwright\async_api\_context_manager.py", line 51, in start
|
||||
return await self.__aenter__()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\py\jjim2\Lib\site-packages\playwright\async_api\_context_manager.py", line 46, in __aenter__
|
||||
playwright = AsyncPlaywright(next(iter(done)).result())
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\py\jjim2\Lib\site-packages\playwright\_impl\_transport.py", line 120, in connect
|
||||
self._proc = await asyncio.create_subprocess_exec(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\Python311\Lib\asyncio\subprocess.py", line 223, in create_subprocess_exec
|
||||
transport, protocol = await loop.subprocess_exec(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\Python311\Lib\asyncio\base_events.py", line 1708, in subprocess_exec
|
||||
transport = await self._make_subprocess_transport(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\Python311\Lib\asyncio\windows_events.py", line 399, in _make_subprocess_transport
|
||||
transp = _WindowsSubprocessTransport(self, protocol, args, shell,
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\Python311\Lib\asyncio\base_subprocess.py", line 36, in __init__
|
||||
self._start(args=args, shell=shell, stdin=stdin, stdout=stdout,
|
||||
File "D:\Python311\Lib\asyncio\windows_events.py", line 929, in _start
|
||||
self._proc = windows_utils.Popen(
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\Python311\Lib\asyncio\windows_utils.py", line 153, in __init__
|
||||
super().__init__(args, stdin=stdin_rfd, stdout=stdout_wfd,
|
||||
File "D:\Python311\Lib\subprocess.py", line 1026, in __init__
|
||||
self._execute_child(args, executable, preexec_fn, close_fds,
|
||||
File "D:\Python311\Lib\subprocess.py", line 1538, in _execute_child
|
||||
hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
FileNotFoundError: [WinError 2] 지정된 파일을 찾을 수 없습니다
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/jjim.ico
BIN
build/jjim.ico
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
3
main.py
3
main.py
|
|
@ -16,6 +16,9 @@ def initialize_com():
|
|||
COINIT_MULTITHREADED = 0x0
|
||||
ctypes.windll.ole32.CoInitializeEx(None, COINIT_MULTITHREADED)
|
||||
|
||||
COINIT_APARTMENTTHREADED = 0x2
|
||||
ctypes.windll.ole32.CoInitializeEx(None, COINIT_APARTMENTTHREADED)
|
||||
|
||||
# COM 해제
|
||||
def uninitialize_com():
|
||||
ctypes.windll.ole32.CoUninitialize()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('src/browsers', 'browsers')],
|
||||
hiddenimports=['playwright', 'PySide6'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='main',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon=['jjim.ico'],
|
||||
)
|
||||
BIN
markets.db
BIN
markets.db
Binary file not shown.
1
setup.py
1
setup.py
|
|
@ -33,6 +33,7 @@ build_exe_options = {
|
|||
"pandas",
|
||||
"PySide6",
|
||||
"playwright",
|
||||
"playwright.async_api",
|
||||
],
|
||||
'include_files': include_files,
|
||||
"excludes": [
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import re
|
|||
import urllib
|
||||
import urllib.parse
|
||||
from PySide6.QtCore import QThread, Signal
|
||||
from playwright.async_api import async_playwright, Page
|
||||
from playwright.async_api import async_playwright, Page, expect
|
||||
from playwright_stealth import stealth_async
|
||||
|
||||
import time
|
||||
|
||||
class Jjim_Runner(QThread):
|
||||
|
|
@ -108,10 +110,10 @@ class Jjim_Runner(QThread):
|
|||
|
||||
base_path = self.get_base_dir()
|
||||
|
||||
# 작업 디렉토리를 base_path로 변경
|
||||
os.chdir(base_path)
|
||||
self.logger.log(f"작업 디렉토리 변경: {base_path}", level=logging.DEBUG)
|
||||
os.environ["PLAYWRIGHT_BROWSERS_PATH"] = os.path.join(base_path, "browsers")
|
||||
# # 작업 디렉토리를 base_path로 변경
|
||||
# os.chdir(base_path)
|
||||
# self.logger.log(f"작업 디렉토리 변경: {base_path}", level=logging.DEBUG)
|
||||
# os.environ["PLAYWRIGHT_BROWSERS_PATH"] = os.path.join(base_path, "browsers")
|
||||
|
||||
browser_path = os.path.join(base_path, 'browsers', 'chromium-1140', 'chrome-win', 'chrome.exe')
|
||||
user_data_dir = os.path.join(base_path, 'browsers', 'user_data')
|
||||
|
|
@ -143,7 +145,8 @@ class Jjim_Runner(QThread):
|
|||
self.playwright = await async_playwright().start()
|
||||
|
||||
# 디버그 모드에 따라 headless 옵션 설정
|
||||
headless_value = False if self.debug_mode else True
|
||||
# headless_value = False if self.debug_mode else True
|
||||
headless_value = False
|
||||
|
||||
# 1. 시크릿 브라우저 실행
|
||||
self.browser = await self.playwright.chromium.launch(
|
||||
|
|
@ -152,26 +155,66 @@ class Jjim_Runner(QThread):
|
|||
args=[
|
||||
'--disable-popup-blocking',
|
||||
'--start-maximized',
|
||||
'--window-size=1920,1080'
|
||||
'--window-size=1920,1080',
|
||||
'--igcognito',
|
||||
'--disable-notifications',
|
||||
'--disable-extensions',
|
||||
'--disable-gpu',
|
||||
'--disable-dev-shm-usage',
|
||||
'--no-sandbox',
|
||||
'--disable-software-rasterizer',
|
||||
'--disable-web-security',
|
||||
'--disable-site-isolation-trials',
|
||||
]
|
||||
)
|
||||
self.context = await self.browser.new_context()
|
||||
self.page = await self.context.new_page()
|
||||
# await stealth_async(self.page)
|
||||
|
||||
# 자동화 흔적 제거 스크립트 추가
|
||||
await self.page.add_init_script("""
|
||||
// 기본 우회: navigator.webdriver, languages, plugins, window.chrome
|
||||
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
||||
Object.defineProperty(navigator, 'languages', { get: () => ['ko-KR', 'en-US', 'en'] });
|
||||
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
|
||||
window.chrome = { runtime: {} };
|
||||
const originalQuery = window.navigator.permissions.query;
|
||||
window.navigator.permissions.query = (parameters) => (
|
||||
parameters.name === 'notifications' ?
|
||||
Promise.resolve({ state: Notification.permission }) :
|
||||
originalQuery(parameters)
|
||||
);
|
||||
|
||||
// 추가 우회: WebGL 정보 모방
|
||||
const getParameter = WebGLRenderingContext.prototype.getParameter;
|
||||
WebGLRenderingContext.prototype.getParameter = function(parameter) {
|
||||
// UNMASKED_VENDOR_WEBGL (37445)와 UNMASKED_RENDERER_WEBGL (37446)을 모방
|
||||
if (parameter === 37445) {
|
||||
return 'Intel Inc.';
|
||||
}
|
||||
if (parameter === 37446) {
|
||||
return 'Intel Iris OpenGL Engine';
|
||||
}
|
||||
return getParameter.call(this, parameter);
|
||||
};
|
||||
|
||||
// 추가 우회: Canvas fingerprint 우회
|
||||
// 예시로 toDataURL 함수를 오버라이드하여 Canvas의 기본 데이터 URL을 그대로 반환합니다.
|
||||
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
||||
HTMLCanvasElement.prototype.toDataURL = function() {
|
||||
// 필요에 따라 캔버스 데이터를 조작할 수 있지만, 여기서는 원본을 그대로 반환합니다.
|
||||
return originalToDataURL.apply(this, arguments);
|
||||
};
|
||||
|
||||
// 추가 우회: WebGLDebugRendererInfo 제거 (일부 사이트는 이 정보로 감지)
|
||||
try {
|
||||
const glProto = WebGLRenderingContext.prototype;
|
||||
const getExtension = glProto.getExtension;
|
||||
glProto.getExtension = function(name) {
|
||||
if (name === 'WEBGL_debug_renderer_info') {
|
||||
return null;
|
||||
}
|
||||
return getExtension.call(this, name);
|
||||
};
|
||||
} catch (e) {
|
||||
// 예외 발생 시 무시
|
||||
}
|
||||
""")
|
||||
|
||||
|
||||
self._initialized = True
|
||||
self.logger.log("브라우저 초기화 완료", level=20)
|
||||
|
||||
|
|
@ -290,13 +333,22 @@ class Jjim_Runner(QThread):
|
|||
|
||||
self.logger.log("로그인 시간 초과", level=logging.WARNING)
|
||||
|
||||
def start_jjim(self, market_lists):
|
||||
def start_jjim(self, market_lists, jjim_settings=None):
|
||||
if jjim_settings is None:
|
||||
# 기본 설정
|
||||
jjim_settings = {
|
||||
'base_interval': 200,
|
||||
'additional_interval': 0,
|
||||
'use_random': False,
|
||||
'new_products_only': False
|
||||
}
|
||||
self.logger.log(f"마켓 리스트: {market_lists}", level=20)
|
||||
asyncio.run_coroutine_threadsafe(self._start_jjim(market_lists), self.loop)
|
||||
self.logger.log(f"찜 설정: {jjim_settings}", level=20)
|
||||
asyncio.run_coroutine_threadsafe(self._start_jjim(market_lists, jjim_settings), self.loop)
|
||||
|
||||
|
||||
|
||||
async def _start_jjim(self, market_lists):
|
||||
async def _start_jjim(self, market_lists, jjim_settings):
|
||||
try:
|
||||
total_markets = len(market_lists)
|
||||
for m_idx, market in enumerate(market_lists):
|
||||
|
|
@ -304,16 +356,29 @@ class Jjim_Runner(QThread):
|
|||
market_url = market.get("market_url")
|
||||
self.logger.log(f"{market_name} 진행 시작", level=logging.INFO)
|
||||
|
||||
# 초기 전체상품 페이지로 이동
|
||||
page_url = market_url.rstrip("/") + "/category/ALL?cp=1"
|
||||
# 상품 페이지로 이동 (신규상품 옵션에 따라 분기)
|
||||
if jjim_settings.get('new_products_only', False):
|
||||
page_url = market_url.rstrip("/") + "/best?cp=1"
|
||||
self.logger.log(f"{market_name} 신규상품 모드 사용", level=logging.DEBUG)
|
||||
else:
|
||||
page_url = market_url.rstrip("/") + "/category/ALL?cp=1"
|
||||
await self.page.goto(page_url)
|
||||
await asyncio.sleep(2) # 페이지 로드 대기
|
||||
# 네트워크 활동이 거의 없는 상태가 될 때까지 대기 (최대 10초)
|
||||
await self.page.wait_for_load_state("networkidle", timeout=10000)
|
||||
self.logger.log(f"{market_name} 초기 페이지 접속 완료", level=logging.DEBUG)
|
||||
# 특정 요소(전체 상품수 정보)가 나타날 때까지 대기 (최대 10초)
|
||||
try:
|
||||
total_products_locator = self.page.locator("#CategoryProducts").get_by_text(re.compile(r'\(총 .*?개\)'))
|
||||
await total_products_locator.wait_for(timeout=10000)
|
||||
self.logger.log(f"{market_name} 전체 상품수 요소 대기 완료", level=logging.DEBUG)
|
||||
except Exception as e:
|
||||
self.logger.log(f"{market_name} 전체 상품수 요소 대기 실패: {e}", level=logging.WARNING)
|
||||
|
||||
# 전체상품수 정보 추출
|
||||
total_info_elem = await self.page.query_selector("div#CategoryProducts span._6lgM26zUO6")
|
||||
total_text = await total_info_elem.inner_text() if total_info_elem else ""
|
||||
match = re.search(r'[\d,]+', total_text)
|
||||
total_products = int(match.group(0).replace(',', '')) if match else 0
|
||||
total_products_locator = self.page.locator("#CategoryProducts").get_by_text(re.compile(r'\(총 .*?개\)'))
|
||||
total_text = await total_products_locator.inner_text() if await total_products_locator.count() > 0 else ""
|
||||
match = re.search(r'\(총 .*?([\d,]+).*?개\)', total_text)
|
||||
total_products = int(match.group(1).replace(',', '')) if match else 0
|
||||
self.logger.log(f"{market_name}의 전체 상품수: {total_products}", level=logging.DEBUG)
|
||||
|
||||
# 진행률 계산용 변수 (이미 처리한 버튼 수, 전체 버튼 수)
|
||||
|
|
@ -325,74 +390,120 @@ class Jjim_Runner(QThread):
|
|||
|
||||
# 페이지 이동은 제공된 "다음" 버튼 정보를 이용
|
||||
while True:
|
||||
# 찜버튼 자체를 모두 선택 (버튼 클래스 "zzim_button" 사용)
|
||||
buttons = await self.page.query_selector_all("div#CategoryProducts button.zzim_button")
|
||||
num_buttons = len(buttons)
|
||||
# 찜버튼 로케이터 설정 (더 정확한 선택자 사용)
|
||||
all_zzim_buttons = self.page.locator("div#CategoryProducts .zzim_button[type='button']")
|
||||
num_buttons = await all_zzim_buttons.count()
|
||||
total_buttons_overall += num_buttons
|
||||
self.logger.log(f"{market_name} 현재 페이지의 찜버튼 개수: {num_buttons}", level=logging.DEBUG)
|
||||
|
||||
# 각 버튼에 대해 처리 (이미 눌린 버튼도 count)
|
||||
for b_idx, btn in enumerate(buttons):
|
||||
processed_buttons += 1 # 모든 버튼은 처리 대상으로 포함
|
||||
# 버튼이 비활성화 상태이면 그냥 진행 (click 시도하지 않음)
|
||||
disabled_attr = await btn.get_attribute("disabled")
|
||||
if disabled_attr is not None:
|
||||
self.logger.log(f"[{market_name}] {b_idx+1}번째 버튼은 비활성화됨", level=logging.DEBUG)
|
||||
else:
|
||||
state = await btn.get_attribute("aria-pressed")
|
||||
# 아직 찜되지 않았다면 클릭 시도
|
||||
if state == "false":
|
||||
try:
|
||||
await btn.click()
|
||||
self.logger.log(f"[{market_name}] {b_idx+1}번째 버튼 클릭", level=logging.DEBUG)
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as click_error:
|
||||
self.logger.log(f"[{market_name}] {b_idx+1}번째 버튼 클릭 오류: {click_error}",
|
||||
level=logging.ERROR, exc_info=True)
|
||||
# 진행률 업데이트: "처리된/전체" 형태로
|
||||
# 각 버튼에 대해 상태를 확인하고 필요시 클릭
|
||||
clicked_count = 0
|
||||
|
||||
for i in range(num_buttons):
|
||||
processed_buttons += 1
|
||||
|
||||
try:
|
||||
button = all_zzim_buttons.nth(i)
|
||||
|
||||
# 버튼이 비활성화되지 않았는지 확인
|
||||
if not await button.is_disabled():
|
||||
# 현재 상태 확인
|
||||
aria_pressed = await button.get_attribute("aria-pressed")
|
||||
|
||||
if aria_pressed == "false":
|
||||
# 아직 찜되지 않은 버튼이므로 클릭
|
||||
await button.click()
|
||||
clicked_count += 1
|
||||
|
||||
# Playwright expect를 사용한 상태 감시 (각 버튼별로)
|
||||
try:
|
||||
# 해당 버튼의 aria-pressed가 "true"가 될 때까지 자동 재시도 (최대 2초)
|
||||
await expect(button).to_have_attribute("aria-pressed", "true", timeout=2000)
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 찜버튼 성공", level=logging.DEBUG)
|
||||
|
||||
except Exception as expect_error:
|
||||
# expect 실패 시 직접 상태 확인으로 fallback
|
||||
try:
|
||||
final_state = await button.get_attribute("aria-pressed")
|
||||
if final_state == "true":
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 찜버튼 성공 (지연 확인)", level=logging.DEBUG)
|
||||
else:
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 찜버튼 상태 확인 미루 (실제로는 찜될 수 있음)", level=logging.DEBUG)
|
||||
except Exception as final_error:
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 찜버튼 상태 확인 오류: {final_error}", level=logging.DEBUG)
|
||||
|
||||
# 찜 간격 조정 (어뷰징 방지)
|
||||
base_interval = jjim_settings.get('base_interval', 200) / 1000.0 # ms를 초로 변환
|
||||
additional_interval = jjim_settings.get('additional_interval', 0) / 1000.0
|
||||
use_random = jjim_settings.get('use_random', False)
|
||||
|
||||
if use_random and additional_interval > 0:
|
||||
# 랜덤 간격: 기본간격 ~ (기본간격 + 추가간격)
|
||||
wait_time = base_interval + random.uniform(0, additional_interval)
|
||||
else:
|
||||
# 고정 간격: 기본간격 + 추가간격
|
||||
wait_time = base_interval + additional_interval
|
||||
|
||||
if wait_time > 0:
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 버튼 처리 후 {wait_time:.2f}초 대기", level=logging.DEBUG)
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
elif aria_pressed == "true":
|
||||
# 이미 찜된 버튼
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 버튼은 이미 찜됨", level=logging.DEBUG)
|
||||
|
||||
else:
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 버튼은 비활성화됨", level=logging.DEBUG)
|
||||
|
||||
except Exception as button_error:
|
||||
self.logger.log(f"[{market_name}] {i+1}번째 버튼 처리 오류: {button_error}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
# 진행률 업데이트
|
||||
progress_text = f"{processed_buttons}/{total_products}"
|
||||
self.product_progress_signal.emit(progress_text)
|
||||
|
||||
# 페이지 이동: 현재 페이지 번호 추출 후 바로 다음 숫자 버튼이 있으면 클릭, 없으면 "다음" 버튼 클릭
|
||||
current_page_elem = await self.page.query_selector("div#CategoryProducts a.UWN4IvaQza._nlog_click[aria-current='true']")
|
||||
if current_page_elem:
|
||||
self.logger.log(f"{market_name} 현재 페이지에서 {clicked_count}개 버튼 클릭 완료", level=logging.DEBUG)
|
||||
|
||||
|
||||
# 페이지 이동: Playwright의 현대적 메서드 활용
|
||||
pagination_container = self.page.locator("div#CategoryProducts div[data-shp-area-id='pgn']")
|
||||
current_page_elem = pagination_container.locator("a[aria-current='true'][role='menuitem']")
|
||||
|
||||
if await current_page_elem.count() > 0:
|
||||
current_page_text = await current_page_elem.inner_text()
|
||||
try:
|
||||
current_page = int(current_page_text.strip())
|
||||
except ValueError:
|
||||
self.logger.log("현재 페이지 번호 변환 오류", level=logging.ERROR)
|
||||
break
|
||||
|
||||
self.logger.log(f"현재 페이지: {current_page}", level=logging.INFO)
|
||||
next_page_clicked = False
|
||||
# 페이지 번호 버튼들에서 현재 번호+1 찾기
|
||||
page_buttons = await self.page.query_selector_all("div#CategoryProducts a.UWN4IvaQza._nlog_click")
|
||||
for btn in page_buttons:
|
||||
text = await btn.inner_text()
|
||||
if text.strip().isdigit():
|
||||
try:
|
||||
page_num = int(text.strip())
|
||||
if page_num == current_page + 1:
|
||||
await btn.click()
|
||||
await asyncio.sleep(0.5)
|
||||
next_page_clicked = True
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
if not next_page_clicked:
|
||||
# 숫자 버튼으로 다음 페이지가 없으면 "다음" 버튼 클릭
|
||||
next_btn = await self.page.query_selector("div#CategoryProducts a.fAUKm1ewwo._2Ar8-aEUTq._nlog_click")
|
||||
if next_btn:
|
||||
hidden = await next_btn.get_attribute("aria-hidden")
|
||||
if hidden == "true":
|
||||
self.logger.log(f"{market_name}의 마지막 페이지 도달", level=logging.DEBUG)
|
||||
break
|
||||
else:
|
||||
await next_btn.click()
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 다음 페이지 번호 계산
|
||||
next_page_number = current_page + 1
|
||||
|
||||
# 다음 페이지 버튼 찾기 (get_by_role과 텍스트 기반)
|
||||
next_page_button = pagination_container.get_by_role("menuitem").filter(has_text=str(next_page_number)).first
|
||||
|
||||
if await next_page_button.count() > 0:
|
||||
await next_page_button.click()
|
||||
await self.page.wait_for_load_state("networkidle", timeout=5000)
|
||||
self.logger.log(f"{market_name} 페이지 {next_page_number}로 이동 완료", level=logging.DEBUG)
|
||||
else:
|
||||
# 숫자 페이지 버튼이 없으면 "다음" 버튼 찾기
|
||||
next_arrow_button = pagination_container.get_by_role("menuitem").filter(has_text="다음").first
|
||||
|
||||
if await next_arrow_button.count() > 0 and not await next_arrow_button.is_hidden():
|
||||
await next_arrow_button.click()
|
||||
await self.page.wait_for_load_state("networkidle", timeout=5000)
|
||||
self.logger.log(f"{market_name} '다음' 버튼으로 이동 완료", level=logging.DEBUG)
|
||||
else:
|
||||
self.logger.log(f"{market_name}의 마지막 페이지 도달", level=logging.DEBUG)
|
||||
break
|
||||
else:
|
||||
self.logger.log("현재 페이지 번호 버튼을 찾을 수 없습니다.", level=logging.ERROR)
|
||||
await self.page.screenshot(path="error_page.png")
|
||||
self.logger.log(f"현재 페이지 주소 : {self.page.url}", level=logging.ERROR)
|
||||
break
|
||||
|
||||
market_progress = int(((m_idx + 1) / total_markets) * 100)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import sys, os, logging
|
||||
from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox,
|
||||
QTextBrowser, QDialog, QProgressBar, QTextEdit, QHBoxLayout, QMenuBar, QMenu)
|
||||
QTextBrowser, QDialog, QProgressBar, QTextEdit, QHBoxLayout, QMenuBar, QMenu,
|
||||
QSpinBox, QCheckBox, QGroupBox, QGridLayout)
|
||||
from PySide6.QtGui import QAction
|
||||
from PySide6.QtCore import Qt, Slot
|
||||
from src.jjim_runner import Jjim_Runner
|
||||
|
|
@ -58,7 +59,8 @@ class MainWindow(QWidget):
|
|||
self.progress_bar.setValue(0)
|
||||
|
||||
# ─────────────────────────────
|
||||
# 1. 상단 버튼 레이아웃 (로그인, 찜하기, 디버그 토글)
|
||||
# 1. 상단 컨트롤 영역
|
||||
# 1-1. 상단 버튼 레이아웃 (로그인, 찜하기, 디버그 토글)
|
||||
self.top_button_layout = QHBoxLayout()
|
||||
self.login_button = QPushButton("로그인")
|
||||
self.login_button.clicked.connect(self.on_login_button_clicked)
|
||||
|
|
@ -67,6 +69,37 @@ class MainWindow(QWidget):
|
|||
self.debug_toggle = ToggleSwitch(label="디버그모드")
|
||||
self.debug_toggle.clicked.connect(self.on_debug_toggle)
|
||||
|
||||
# 1-2. 찜 설정 그룹박스
|
||||
self.jjim_settings_group = QGroupBox("찜 설정")
|
||||
jjim_settings_layout = QGridLayout()
|
||||
|
||||
# 찜 간격 설정
|
||||
jjim_settings_layout.addWidget(QLabel("기본 찜 간격 (ms):"), 0, 0)
|
||||
self.base_interval_spin = QSpinBox()
|
||||
self.base_interval_spin.setRange(100, 2000)
|
||||
self.base_interval_spin.setValue(200)
|
||||
self.base_interval_spin.setSuffix(" ms")
|
||||
jjim_settings_layout.addWidget(self.base_interval_spin, 0, 1)
|
||||
|
||||
jjim_settings_layout.addWidget(QLabel("추가 간격 (ms):"), 1, 0)
|
||||
self.additional_interval_spin = QSpinBox()
|
||||
self.additional_interval_spin.setRange(0, 3000)
|
||||
self.additional_interval_spin.setValue(0)
|
||||
self.additional_interval_spin.setSuffix(" ms")
|
||||
jjim_settings_layout.addWidget(self.additional_interval_spin, 1, 1)
|
||||
|
||||
# 랜덤 버튼
|
||||
self.random_interval_checkbox = QCheckBox("랜덤 간격 사용")
|
||||
self.random_interval_checkbox.setToolTip("기본간격~(기본간격+추가간격) 사이에서 랜덤하게 대기")
|
||||
jjim_settings_layout.addWidget(self.random_interval_checkbox, 2, 0, 1, 2)
|
||||
|
||||
# 신규상품만 찜하기 옵션
|
||||
self.new_products_only_checkbox = QCheckBox("신규상품만 찜하기")
|
||||
self.new_products_only_checkbox.setToolTip("체크하면 /best 페이지에서 신규상품만 찜합니다")
|
||||
jjim_settings_layout.addWidget(self.new_products_only_checkbox, 3, 0, 1, 2)
|
||||
|
||||
self.jjim_settings_group.setLayout(jjim_settings_layout)
|
||||
|
||||
# 스타일 적용 (모던한 폰트, 패딩 등)
|
||||
self.button_style = """
|
||||
QPushButton {
|
||||
|
|
@ -103,7 +136,10 @@ class MainWindow(QWidget):
|
|||
self.top_button_layout.addWidget(self.jjim_button)
|
||||
self.top_button_layout.addWidget(self.debug_toggle)
|
||||
self.top_button_layout.addStretch()
|
||||
|
||||
# 상단 컨트롤 영역을 메인 레이아웃에 추가
|
||||
self.main_layout.addLayout(self.top_button_layout)
|
||||
self.main_layout.addWidget(self.jjim_settings_group)
|
||||
|
||||
# ─────────────────────────────
|
||||
# 2. 마켓 관리 영역: 입력란 및 "마켓 추가" 버튼, 마켓 목록 위젯
|
||||
|
|
@ -247,13 +283,21 @@ class MainWindow(QWidget):
|
|||
QMessageBox.warning(self, "오류", "등록된 마켓 정보가 없습니다.")
|
||||
return
|
||||
market_lists = market_df.to_dict(orient="records")
|
||||
self.jjim_runner.start_jjim(market_lists)
|
||||
|
||||
# 찜 설정 값 가져오기
|
||||
jjim_settings = {
|
||||
'base_interval': self.base_interval_spin.value(),
|
||||
'additional_interval': self.additional_interval_spin.value(),
|
||||
'use_random': self.random_interval_checkbox.isChecked(),
|
||||
'new_products_only': self.new_products_only_checkbox.isChecked()
|
||||
}
|
||||
self.jjim_runner.start_jjim(market_lists, jjim_settings)
|
||||
|
||||
@Slot(bool)
|
||||
def on_debug_toggle(self, state):
|
||||
mode = "ON" if state else "OFF"
|
||||
self.logger.log(f"디버그모드 {mode}", level=logging.INFO)
|
||||
self.jjim_runner.set_debug_mode(state)
|
||||
# self.jjim_runner.set_debug_mode(state)
|
||||
|
||||
@Slot(bool, str)
|
||||
def on_jjim_complete(self, success, message):
|
||||
|
|
|
|||
|
|
@ -144,6 +144,9 @@ class MarketManagerWidget(QWidget):
|
|||
self.confirm_and_delete(item)
|
||||
|
||||
def eventFilter(self, source, event):
|
||||
if isinstance(source, QLineEdit):
|
||||
return False
|
||||
|
||||
if source is self.market_list_widget:
|
||||
if event.type() == QEvent.KeyPress:
|
||||
if event.key() == Qt.Key_Delete:
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 4.0 KiB |
BIN
temp_qr.png
BIN
temp_qr.png
Binary file not shown.
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.0 KiB |
Loading…
Reference in New Issue