Compare commits

...

No commits in common. "master" and "new_test" have entirely different histories.

22 changed files with 2062 additions and 203 deletions

4
.gitignore vendored
View File

@ -6,4 +6,6 @@ pyvenv.cfg
src/browsers/cache/
src/browsers/user_data/
login/__pycache__/
src/__pycache__/
src/__pycache__/
build/
dist/

39
build.py Normal file
View File

@ -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()

View File

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

View File

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

Before

Width:  |  Height:  |  Size: 6.4 KiB

BIN
error_page.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

1739
jjim2.log

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -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()

39
main.spec Normal file
View File

@ -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'],
)

Binary file not shown.

View File

@ -33,6 +33,7 @@ build_exe_options = {
"pandas",
"PySide6",
"playwright",
"playwright.async_api",
],
'include_files': include_files,
"excludes": [

View File

@ -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)
self.logger.log(f"{market_name} 현재 페이지에서 {clicked_count}개 버튼 클릭 완료", level=logging.DEBUG)
# 페이지 이동: 현재 페이지 번호 추출 후 바로 다음 숫자 버튼이 있으면 클릭, 없으면 "다음" 버튼 클릭
current_page_elem = await self.page.query_selector("div#CategoryProducts a.UWN4IvaQza._nlog_click[aria-current='true']")
if current_page_elem:
# 페이지 이동: 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:
break
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)

View File

@ -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)
@ -66,6 +68,37 @@ class MainWindow(QWidget):
self.jjim_button.clicked.connect(self.on_jjim_button_clicked)
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 = """
@ -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):

View File

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

BIN
src/temp_qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB