This commit is contained in:
9700X_PC 2025-03-25 21:09:03 +09:00
parent a0689ddb87
commit 0160e89f49
17 changed files with 4389 additions and 143 deletions

View File

@ -0,0 +1,63 @@
## 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.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@ -0,0 +1,59 @@
[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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
jjim.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

3946
jjim2.log

File diff suppressed because it is too large Load Diff

BIN
libs/cx_Logging.lib Normal file

Binary file not shown.

View File

@ -2,6 +2,7 @@
from PySide6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton
from PySide6.QtGui import QPixmap, QFont
from PySide6.QtCore import QTimer, QCoreApplication, Qt
import re
class QRDialog(QDialog):
"""
@ -29,11 +30,36 @@ class QRDialog(QDialog):
self.info_label.setFont(QFont("Arial", 12, QFont.Bold))
layout.addWidget(self.info_label)
# 로그인 안내 메시지
self.additional_info_label = QLabel(inner_text)
self.additional_info_label.setAlignment(Qt.AlignCenter)
self.additional_info_label.setFont(QFont("Arial", 10, QFont.Bold))
layout.addWidget(self.additional_info_label)
match = re.search(r'(\d+)', inner_text)
if match:
number_str = match.group(1)
# 최대 한 번 분리하여 앞, 뒤로 나눔
parts = re.split(r'\d+', inner_text, maxsplit=1)
text_before = parts[0].strip()
text_after = parts[1].strip() if len(parts) > 1 else ""
else:
number_str = ""
text_before = inner_text
text_after = ""
# 앞부분 텍스트 라벨
self.before_label = QLabel(text_before)
self.before_label.setAlignment(Qt.AlignCenter)
self.before_label.setFont(QFont("Arial", 10))
layout.addWidget(self.before_label)
# 추출한 숫자를 크게 굵게 표시하는 라벨
self.number_label = QLabel(number_str)
self.number_label.setAlignment(Qt.AlignCenter)
self.number_label.setFont(QFont("Arial", 30, QFont.Bold))
layout.addWidget(self.number_label)
# 뒷부분 텍스트 라벨
self.after_label = QLabel(text_after)
self.after_label.setAlignment(Qt.AlignCenter)
self.after_label.setFont(QFont("Arial", 10))
layout.addWidget(self.after_label)
# 카운트다운 라벨
self.countdown_label = QLabel()
@ -68,7 +94,8 @@ class QRDialog(QDialog):
else:
self.timer.stop()
self.reject()
class LoginSuccessDialog(QDialog):
"""로그인 성공 시 표시되는 다이얼로그 (1초 후 자동 닫힘)"""

View File

@ -1,6 +1,8 @@
import sys
import logging
from PySide6.QtWidgets import QApplication
from PySide6.QtGui import QIcon
from src.main_window import MainWindow
from src.logger_module import Logger
from login.login_dialog import LoginDialog
@ -24,6 +26,7 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
app.setWindowIcon(QIcon("jjim.ico"))
# 로그인 다이얼로그 실행 (모달)
login_dialog = LoginDialog(logger)

60
setup.py Normal file
View File

@ -0,0 +1,60 @@
# setup.py
import sys
import os
from cx_Freeze import setup, Executable
base = None
if sys.platform == "win32":
base = "Win32GUI"
# 필요한 파일 경로 설정
base_dir = os.path.dirname(__file__)
browsers_dir = os.path.join(base_dir, 'src', 'browsers')
chromium_dir = os.path.join(browsers_dir, 'src', 'browsers', 'chromium-1140')
# ✅ 기존 포함 파일 + DLL 추가
include_files = [
('src/browsers/chromium-1140', 'lib/src/browsers/chromium-1140'),
('jjim.ico', 'jjim.ico'),
]
for src, dest in include_files:
if not os.path.exists(src):
print(f"경로가 존재하지 않습니다: {src}")
build_exe_options = {
"packages": [
"asyncio",
"os",
"sys",
"re",
"logging",
"sqlite3",
"pandas",
"PySide6",
"playwright",
],
'include_files': include_files,
"excludes": [
],
"optimize": 1,
"zip_include_packages": ["*"],
"zip_exclude_packages": [],
}
executables = [
Executable(
script="main.py",
base=base,
target_name="jjim.exe",
icon="jjim.ico"
)
]
setup(
name="Jjim",
version="1.0",
description="찜기",
options={"build_exe": build_exe_options},
executables=executables
)

View File

@ -24,12 +24,13 @@ class Jjim_Runner(QThread):
jjim_complete = Signal(bool, str)
market_progress_signal = Signal(int)
product_progress_signal = Signal(int)
product_progress_signal = Signal(str)
def __init__(self, logger, db_manager):
def __init__(self, logger, db_manager, debug_mode):
super().__init__()
self.logger = logger
self.db_manager = db_manager
self.debug_mode = debug_mode # 디버그 모드 여부 (True이면 headless False)
self.browser = None
self.context = None
self.page = None
@ -38,6 +39,9 @@ class Jjim_Runner(QThread):
self.loop = None
self._initialized = False
def set_debug_mode(self, debug_mode):
self.debug_mode = debug_mode
async def monitor_login(self, page, login_url, timeout=90):
"""
1초마다 현재 URL을 감시하여, QR 페이지 URL과 달라지면 로그인 완료로 판단.
@ -64,19 +68,35 @@ class Jjim_Runner(QThread):
self.loop.run_forever()
self.loop.close()
# def get_base_dir(self):
# """
# 실행 환경에 따라 base_dir을 설정하는 메서드.
# cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
# """
# if getattr(sys, 'frozen', False): # 패키징된 경우
# base_dir = os.path.dirname(sys.executable)
# internal_dir = os.path.join(base_dir, 'lib') # _internal 디렉토리 포함
# if os.path.exists(internal_dir): # _internal 디렉토리가 존재하면 base_dir로 설정
# return internal_dir
# else: # 일반 Python 실행 환경
# base_dir = os.path.dirname(os.path.abspath(__file__))
# return base_dir
def get_base_dir(self):
"""
실행 환경에 따라 base_dir을 설정하는 메서드.
cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
패키징된 경우, 실행 파일의 위치 또는 실행 파일이 있는 폴더 아래의 'lib' 폴더를 기준으로 합니다.
개발 환경에서는 src 폴더의 상위 디렉토리를 기준으로 합니다.
"""
if getattr(sys, 'frozen', False): # 패키징된 경우
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
internal_dir = os.path.join(base_dir, '_internal') # _internal 디렉토리 포함
if os.path.exists(internal_dir): # _internal 디렉토리가 존재하면 base_dir로 설정
return internal_dir
else: # 일반 Python 실행 환경
base_dir = os.path.dirname(os.path.abspath(__file__))
# 패키징된 결과물에서는 browsers 폴더가 base_dir이 아니라 base_dir/lib 에 있을 수 있음.
if not os.path.exists(os.path.join(base_dir, "browsers")):
alt_dir = os.path.join(base_dir, "lib")
if os.path.exists(os.path.join(alt_dir, "browsers")):
base_dir = alt_dir
else:
base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)))
return base_dir
async def start_browser(self):
@ -87,6 +107,12 @@ class Jjim_Runner(QThread):
try:
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")
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')
cache_dir = os.path.join(base_path, 'browsers', 'cache')
@ -116,42 +142,21 @@ class Jjim_Runner(QThread):
self.playwright = await async_playwright().start()
# 디버그 모드에 따라 headless 옵션 설정
headless_value = False if self.debug_mode else True
# # 1. 시크릿 브라우저 실행
# self.browser = await self.playwright.chromium.launch(
# headless=True,
# executable_path=browser_path,
# args=[
# '--disable-popup-blocking',
# '--start-maximized',
# '--window-size=1920,1080'
# ]
# )
# self.context = await self.browser.new_context()
# self.page = await self.context.new_page()
# 사용자설정 브라우저 실행
self.browser = await self.playwright.chromium.launch_persistent_context(
user_data_dir=user_data_dir,
headless=False,
# 1. 시크릿 브라우저 실행
self.browser = await self.playwright.chromium.launch(
headless=headless_value,
executable_path=browser_path,
permissions=["geolocation", "notifications"],
geolocation={"latitude": 37.5665, "longitude": 126.9780},
locale="ko-KR",
args=[
'--disable-popup-blocking',
'--start-maximized',
'--window-size=1920,1080'
],
user_agent=self.user_agent
]
)
# launch_persistent_context()는 바로 페이지를 생성할 수 있으므로:
self.page = await self.browser.new_page()
# 첫 번째 기본 탭 닫기
if self.browser.pages:
await self.browser.pages[0].close()
self.context = await self.browser.new_context()
self.page = await self.context.new_page()
# 자동화 흔적 제거 스크립트 추가
await self.page.add_init_script("""
@ -169,18 +174,42 @@ class Jjim_Runner(QThread):
self._initialized = True
self.logger.log("브라우저 초기화 완료", level=20)
# await self.page.goto("https://smartstore.naver.com/tam_design")
except Exception as e:
self.logger.log(f"브라우저 초기화 오류: {e}", level=40)
self.logger.log(f"브라우저 초기화 오류: {e}", level=40, exc_info=True)
def start_login(self):
"""
메인윈도우에서 로그인 버튼 클릭 호출.
브라우저 초기화가 완료될 때까지 기다린 로그인 시도를 시작합니다.
QThread의 이벤트 루프에서 _start_login() 코루틴을 실행합니다.
"""
if not self._initialized:
self.logger.log("브라우저가 초기화되지 않음", level=40)
return
asyncio.run_coroutine_threadsafe(self._start_login(), self.loop)
import time
# 최대 3초간 self.loop가 생성될 때까지 기다림
timeout = 3
start_time = time.time()
while self.loop is None:
if time.time() - start_time > timeout:
self.logger.log("start_login: 이벤트 루프 초기화 대기 시간 초과", level=logging.ERROR)
return
time.sleep(0.1)
async def wait_for_init(timeout=30):
start = asyncio.get_event_loop().time()
while not self._initialized:
await asyncio.sleep(0.1)
if asyncio.get_event_loop().time() - start > timeout:
raise TimeoutError("브라우저 초기화 대기 시간 초과")
async def start_login_when_ready():
try:
await wait_for_init()
await self._start_login()
except Exception as e:
self.logger.log(f"start_login 에러: {e}", level=logging.ERROR)
asyncio.run_coroutine_threadsafe(start_login_when_ready(), self.loop)
async def _start_login(self):
"""
@ -194,7 +223,7 @@ class Jjim_Runner(QThread):
try:
await self.page.wait_for_selector("img#qrImage", timeout=10000)
except Exception as e:
self.logger.log(f"QR 코드 로드 오류: {e}", level=40)
self.logger.log(f"QR 코드 로드 오류: {e}", level=40, exc_info=True)
return
await asyncio.sleep(2) # QR 코드 렌더링 대기
qrImage = await self.page.query_selector("img#qrImage")
@ -265,79 +294,112 @@ class Jjim_Runner(QThread):
self.logger.log(f"마켓 리스트: {market_lists}", level=20)
asyncio.run_coroutine_threadsafe(self._start_jjim(market_lists), self.loop)
async def _start_jjim(self, market_lists):
try:
total_markets = len(market_lists)
# 전체 마켓 진행률 초기화
for m_idx, market in enumerate(market_lists):
market_name = market.get("market_name")
market_url = market.get("market_url")
self.logger.log(f"{market_name} 진행 시작", level=logging.INFO)
# 전체상품 페이지 구성
cp = 1
page_url = market_url.rstrip("/") + f"/category/ALL?cp={cp}"
# 초기 전체상품 페이지로 이동
page_url = market_url.rstrip("/") + "/category/ALL?cp=1"
await self.page.goto(page_url)
await asyncio.sleep(2)
# 전체상품수 요소 추출 (예: <span class="_6lgM26zUO6">(총 <strong>4,061</strong>개)</span>)
await asyncio.sleep(2) # 페이지 로드 대기
# 전체상품수 정보 추출
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
self.logger.log(f"{market_name}의 전체 상품수: {total_products}", level=logging.DEBUG)
clicked_count = 0
# 현재 마켓 내 상품 찜 진행률 초기화
self.product_progress_signal.emit(0)
# 진행률 계산용 변수 (이미 처리한 버튼 수, 전체 버튼 수)
processed_buttons = 0
total_buttons_overall = 0
# 초기 상품 진행률 표시 (예: "0/0")
self.product_progress_signal.emit(f"{processed_buttons}/{total_products}")
# 페이지 이동은 제공된 "다음" 버튼 정보를 이용
while True:
# 페이지 내 제품 리스트 선택 (예: li 요소)
products = await self.page.query_selector_all("div#CategoryProducts li")
total_products_page = len(products)
self.logger.log(f"{market_name} cp={cp}의 상품수: {total_products_page}", level=logging.DEBUG)
# 찜버튼 자체를 모두 선택 (버튼 클래스 "zzim_button" 사용)
buttons = await self.page.query_selector_all("div#CategoryProducts button.zzim_button")
num_buttons = len(buttons)
total_buttons_overall += num_buttons
self.logger.log(f"{market_name} 현재 페이지의 찜버튼 개수: {num_buttons}", level=logging.DEBUG)
for p_idx, product in enumerate(products):
btn = await product.query_selector("button[type='button']")
if btn is None:
continue
# 만약 버튼에 disabled 속성이 있다면 클릭하지 않음
# 각 버튼에 대해 처리 (이미 눌린 버튼도 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:
continue
# aria-pressed 상태 확인
state = await btn.get_attribute("aria-pressed")
if state == "false":
try:
await btn.click()
clicked_count += 1
self.logger.log(f"[{market_name}] {p_idx+1}번 상품 찜하기 클릭", level=logging.DEBUG)
await asyncio.sleep(1) # 너무 빠르지 않게
except Exception as click_error:
self.logger.log(f"[{market_name}] {p_idx+1}번 상품 클릭 오류: {click_error}", level=logging.ERROR, exc_info=True)
# 현재 마켓 내 찜 진행률 업데이트
if total_products > 0:
prod_progress = int((clicked_count / total_products) * 100)
self.product_progress_signal.emit(prod_progress)
# 다음 페이지 존재 여부 확인
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
self.logger.log(f"[{market_name}] {b_idx+1}번째 버튼은 비활성화됨", level=logging.DEBUG)
else:
await next_btn.click()
cp += 1
await asyncio.sleep(2)
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)
# 진행률 업데이트: "처리된/전체" 형태로
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:
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)
else:
break
else:
self.logger.log("현재 페이지 번호 버튼을 찾을 수 없습니다.", level=logging.ERROR)
break
# 마켓 진행률 업데이트 (전체 마켓 기준)
market_progress = int(((m_idx + 1) / total_markets) * 100)
self.market_progress_signal.emit(market_progress)
self.logger.log(f"{market_name} 처리 완료", level=logging.DEBUG)
self.jjim_complete.emit(True, "모든 마켓의 찜하기 작업 완료")
self.logger.log(f"{market_name} 처리 완료: 처리된 버튼 {processed_buttons} / 전체 버튼 {total_products}", level=logging.DEBUG)
self.jjim_complete.emit(True, f"[{total_markets}]개 마켓의 [{total_buttons_overall}]개 상품찜 하기 작업 완료 ")
except Exception as e:
self.logger.log(f"찜 중 오류: {e}", level=logging.ERROR, exc_info=True)
self.jjim_complete.emit(False, f"찜 중 오류: {e}")
self.jjim_complete.emit(False, f"찜 중 오류: {e}", exc_info=True)

View File

@ -27,25 +27,25 @@ class MainWindow(QWidget):
self.resize(1000, 700)
self.main_layout = QVBoxLayout()
# 메뉴바 생성
self.menu_bar = QMenuBar(self)
# # 메뉴바 생성
# self.menu_bar = QMenuBar(self)
# 설정 메뉴
self.settings_menu = QMenu("설정", self)
self.menu_bar.addMenu(self.settings_menu)
# # 설정 메뉴
# self.settings_menu = QMenu("설정", self)
# self.menu_bar.addMenu(self.settings_menu)
# 도움말 메뉴
self.help_menu = QMenu("도움말", self)
self.menu_bar.addMenu(self.help_menu)
# # 도움말 메뉴
# self.help_menu = QMenu("도움말", self)
# self.menu_bar.addMenu(self.help_menu)
# 도움말 메뉴에 항목 추가
self.help_action = QAction("도움말 보기", self)
self.help_action.triggered.connect(self.show_help_dialog)
self.help_menu.addAction(self.help_action)
# # 도움말 메뉴에 항목 추가
# self.help_action = QAction("도움말 보기", self)
# self.help_action.triggered.connect(self.show_help_dialog)
# self.help_menu.addAction(self.help_action)
# 사용자 정보 메뉴
self.user_menu = QMenu("사용자 정보", self)
self.menu_bar.addMenu(self.user_menu)
# # 사용자 정보 메뉴
# self.user_menu = QMenu("사용자 정보", self)
# self.menu_bar.addMenu(self.user_menu)
# 로그창 추가
self.log_display = QTextEdit()
@ -68,7 +68,7 @@ class MainWindow(QWidget):
self.debug_toggle.clicked.connect(self.on_debug_toggle)
# 스타일 적용 (모던한 폰트, 패딩 등)
button_style = """
self.button_style = """
QPushButton {
font-size: 14px;
padding: 8px 16px;
@ -81,8 +81,24 @@ class MainWindow(QWidget):
background-color: #005F99;
}
"""
self.login_button.setStyleSheet(button_style)
self.jjim_button.setStyleSheet(button_style)
# 로그인 완료 스타일 적용 (모던한 폰트, 패딩 등)
self.complete_button_style = """
QPushButton {
font-size: 14px;
padding: 8px 16px;
background-color: #AAAAAA; /* 회색 배경 */
color: white;
border: none;
border-radius: 4px;
}
QPushButton:hover {
background-color: #888888; /* 마우스 오버 시 좀 더 어두운 회색 */
}
"""
self.login_button.setStyleSheet(self.button_style)
self.jjim_button.setStyleSheet(self.complete_button_style)
self.top_button_layout.addWidget(self.login_button)
self.top_button_layout.addWidget(self.jjim_button)
self.top_button_layout.addWidget(self.debug_toggle)
@ -137,8 +153,16 @@ class MainWindow(QWidget):
# GUI 로그 연결
self.logger.set_gui_logger(self.append_gui_log)
self.jjim_runner = None
@Slot()
def on_login_button_clicked(self):
self.logger.log("로그인 버튼 클릭 - QR 로그인 요청", level=logging.INFO)
debug_mode = self.debug_toggle.isChecked()
# jjim_runner QThread 인스턴스 생성 및 브라우저 초기화
self.jjim_runner = Jjim_Runner(self.logger, self.db_manager)
self.jjim_runner = Jjim_Runner(self.logger, self.db_manager, debug_mode)
self.jjim_runner.login_ready.connect(self.on_qr_ready)
self.jjim_runner.login_complete.connect(self.on_login_complete)
self.jjim_runner.login_in_progress.connect(self.on_login_in_progress)
@ -148,10 +172,6 @@ class MainWindow(QWidget):
self.jjim_runner.start()
@Slot()
def on_login_button_clicked(self):
self.logger.log("로그인 버튼 클릭 - QR 로그인 요청", level=logging.INFO)
# 로그인 프로세스 시작 전에 잠시 기다리라는 메시지창을 띄웁니다.
self.show_wait_message("잠시만 기다려주세요.\nQR 코드 로딩 중입니다...")
@ -207,9 +227,16 @@ class MainWindow(QWidget):
success_dialog = LoginSuccessDialog(self)
success_dialog.exec()
# 로그인 완료 후 버튼 활성화
self.login_button.setEnabled(False)
self.login_button.setStyleSheet(self.complete_button_style)
self.login_button.setText("로그인 완료")
self.jjim_button.setEnabled(True)
self.jjim_button.setStyleSheet(self.button_style)
@Slot()
def on_jjim_button_clicked(self):
@ -226,6 +253,7 @@ class MainWindow(QWidget):
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)
@Slot(bool, str)
def on_jjim_complete(self, success, message):
@ -234,27 +262,25 @@ class MainWindow(QWidget):
else:
QMessageBox.warning(self, "찜찜 실패", message)
def get_base_dir(self):
"""
실행 환경에 따라 base_dir을 설정하는 메서드.
"""
if getattr(sys, 'frozen', False): # 패키징된 경우
base_dir = os.path.dirname(sys.executable)
internal_dir = os.path.join(base_dir, 'src') # _internal 디렉토리 포함
if os.path.exists(internal_dir):
return internal_dir
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
return base_dir
@Slot(int)
def update_market_progress(self, value):
self.market_progress_bar.setValue(value)
@Slot(int)
def update_product_progress(self, value):
self.product_progress_bar.setValue(value)
@Slot(str)
def update_product_progress(self, progress_text):
"""
progress_text: "현재/전체" 형식의 문자열 ) "3/59"
"""
try:
current, total = progress_text.split('/')
current = int(current)
total = int(total)
percentage = int((current / total) * 100) if total != 0 else 0
self.product_progress_bar.setValue(percentage)
self.product_progress_bar.setFormat(f"{progress_text} ({percentage}%)")
except Exception as e:
self.product_progress_bar.setValue(0)
self.product_progress_bar.setFormat("0/0 (0%)")
def append_gui_log(self, message):
self.log_display.append(message)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB