...
This commit is contained in:
parent
eb9c0834bb
commit
ee77982bc7
200
app.log
200
app.log
|
|
@ -37,3 +37,203 @@
|
|||
[2025-03-28 09:32:26,373] [DEBUG] Exiting LoginDialog.create_dialog()
|
||||
[2025-03-28 09:32:26,373] [DEBUG] LoginDialog.show() 호출됨
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
[2025-03-28 12:32:42,342] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:32:43,030] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:32:43,031] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:32:45,175] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 12:32:45,176] [WARNING] 로그인 실패
|
||||
[2025-03-28 12:32:48,280] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 12:32:48,281] [WARNING] 로그인 실패
|
||||
[2025-03-28 12:37:16,267] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:37:16,268] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:37:16,899] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:37:16,900] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:37:56,645] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:37:56,646] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:37:57,265] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:37:57,266] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:38:26,308] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 12:38:26,309] [DEBUG] Entering DBManager.login()
|
||||
[2025-03-28 12:38:26,535] [DEBUG] Entering update_client_with_token()
|
||||
[2025-03-28 12:38:26,943] [DEBUG] Client updated with JWT token
|
||||
[2025-03-28 12:38:26,945] [DEBUG] Exiting update_client_with_token()
|
||||
[2025-03-28 12:38:26,945] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 12:38:26,946] [DEBUG] Exiting DBManager.login()
|
||||
[2025-03-28 12:38:26,947] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 12:38:26,947] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
|
||||
[2025-03-28 12:38:27,530] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
|
||||
[2025-03-28 12:38:27,531] [DEBUG] Exiting update_last_login()
|
||||
[2025-03-28 12:39:16,830] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:39:16,847] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:39:17,461] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:39:17,462] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:39:36,067] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 12:39:36,068] [DEBUG] Entering DBManager.login()
|
||||
[2025-03-28 12:39:36,324] [DEBUG] Entering update_client_with_token()
|
||||
[2025-03-28 12:39:36,762] [DEBUG] Client updated with JWT token
|
||||
[2025-03-28 12:39:36,762] [DEBUG] Exiting update_client_with_token()
|
||||
[2025-03-28 12:39:36,763] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 12:39:36,764] [DEBUG] Exiting DBManager.login()
|
||||
[2025-03-28 12:39:36,765] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 12:39:36,765] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
|
||||
[2025-03-28 12:39:37,390] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
|
||||
[2025-03-28 12:39:37,391] [DEBUG] Exiting update_last_login()
|
||||
[2025-03-28 12:39:37,393] [DEBUG] MarketPage initialized
|
||||
[2025-03-28 12:41:53,495] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:41:53,499] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:41:54,130] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:41:54,131] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:42:25,102] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 12:42:25,103] [DEBUG] Entering DBManager.login()
|
||||
[2025-03-28 12:42:25,242] [DEBUG] Entering update_client_with_token()
|
||||
[2025-03-28 12:42:25,649] [DEBUG] Client updated with JWT token
|
||||
[2025-03-28 12:42:25,650] [DEBUG] Exiting update_client_with_token()
|
||||
[2025-03-28 12:42:25,651] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 12:42:25,651] [DEBUG] Exiting DBManager.login()
|
||||
[2025-03-28 12:42:25,652] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 12:42:25,653] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
|
||||
[2025-03-28 12:42:26,193] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
|
||||
[2025-03-28 12:42:26,194] [DEBUG] Exiting update_last_login()
|
||||
[2025-03-28 12:42:26,196] [DEBUG] MarketPage initialized
|
||||
[2025-03-28 12:42:26,200] [DEBUG] MarketPage.load_market_list() 호출됨
|
||||
[2025-03-28 12:42:26,200] [DEBUG] Entering get_market_list()
|
||||
[2025-03-28 12:42:27,201] [DEBUG] Market list retrieved: [{'name': 'Market A', 'url': 'https://market-a.com', 'memo': 'Memo A'}, {'name': 'Market B', 'url': 'https://market-b.com', 'memo': 'Memo B'}]
|
||||
[2025-03-28 12:42:27,202] [DEBUG] Exiting get_market_list()
|
||||
[2025-03-28 12:45:05,179] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:45:05,179] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:45:05,807] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:45:05,808] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:48:32,757] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:48:32,759] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:48:33,378] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:48:33,379] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:49:14,007] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:49:14,010] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:49:14,627] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:49:14,629] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:50:06,883] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:50:06,885] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:50:07,517] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:50:07,517] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 12:51:29,299] [DEBUG] Logger initialized
|
||||
[2025-03-28 12:51:29,300] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 12:51:29,950] [DEBUG] DBManager initialized
|
||||
[2025-03-28 12:51:29,951] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:02:53,368] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:02:53,369] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:02:54,057] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:02:54,057] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:04:46,339] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:04:46,341] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:04:46,958] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:04:46,959] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:05:53,638] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:05:53,640] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:05:54,324] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:05:54,324] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:06:05,877] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 13:06:05,878] [DEBUG] Entering DBManager.login()
|
||||
[2025-03-28 13:06:06,167] [DEBUG] Entering update_client_with_token()
|
||||
[2025-03-28 13:06:06,596] [DEBUG] Client updated with JWT token
|
||||
[2025-03-28 13:06:06,597] [DEBUG] Exiting update_client_with_token()
|
||||
[2025-03-28 13:06:06,598] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 13:06:06,598] [DEBUG] Exiting DBManager.login()
|
||||
[2025-03-28 13:06:06,599] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 13:06:06,599] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
|
||||
[2025-03-28 13:06:07,284] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
|
||||
[2025-03-28 13:06:07,284] [DEBUG] Exiting update_last_login()
|
||||
[2025-03-28 13:06:07,288] [DEBUG] MarketPage initialized
|
||||
[2025-03-28 13:06:07,292] [DEBUG] MarketPage.load_market_list() 호출됨
|
||||
[2025-03-28 13:06:07,293] [DEBUG] Entering get_market_list()
|
||||
[2025-03-28 13:06:08,295] [DEBUG] Market list retrieved: [{'name': 'Market A', 'url': 'https://market-a.com', 'memo': 'Memo A'}, {'name': 'Market B', 'url': 'https://market-b.com', 'memo': 'Memo B'}]
|
||||
[2025-03-28 13:06:08,296] [DEBUG] Exiting get_market_list()
|
||||
[2025-03-28 13:08:43,321] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:08:43,324] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:08:43,952] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:08:43,953] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:13:09,363] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:13:09,365] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:13:09,992] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:13:46,986] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:13:46,987] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:13:47,623] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:13:47,623] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:15:37,273] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:15:37,274] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:15:37,947] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:15:37,947] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:16:14,265] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:16:14,265] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:16:14,738] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:16:14,739] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:17:11,858] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:17:11,858] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:17:12,481] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:17:12,481] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:22:45,886] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:22:45,887] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:22:46,514] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:22:46,514] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:23:10,131] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:23:10,132] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:23:10,767] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:23:10,768] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:23:26,381] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:23:26,382] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:23:27,004] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:23:27,005] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:23:42,158] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 13:23:42,159] [DEBUG] Entering DBManager.login()
|
||||
[2025-03-28 13:23:42,345] [DEBUG] Entering update_client_with_token()
|
||||
[2025-03-28 13:23:42,777] [DEBUG] Client updated with JWT token
|
||||
[2025-03-28 13:23:42,778] [DEBUG] Exiting update_client_with_token()
|
||||
[2025-03-28 13:23:42,778] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 13:23:42,779] [DEBUG] Exiting DBManager.login()
|
||||
[2025-03-28 13:23:42,780] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 13:23:42,780] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
|
||||
[2025-03-28 13:23:43,582] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
|
||||
[2025-03-28 13:23:43,582] [DEBUG] Exiting update_last_login()
|
||||
[2025-03-28 13:23:43,586] [DEBUG] MarketPage initialized
|
||||
[2025-03-28 13:23:43,589] [DEBUG] MarketPage.load_market_list() 호출됨
|
||||
[2025-03-28 13:23:43,590] [DEBUG] Entering get_market_list()
|
||||
[2025-03-28 13:23:44,591] [DEBUG] Market list retrieved: [{'name': 'Market A', 'url': 'https://market-a.com', 'memo': 'Memo A'}, {'name': 'Market B', 'url': 'https://market-b.com', 'memo': 'Memo B'}]
|
||||
[2025-03-28 13:23:44,593] [DEBUG] Exiting get_market_list()
|
||||
[2025-03-28 13:59:40,064] [DEBUG] Logger initialized
|
||||
[2025-03-28 13:59:40,064] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 13:59:40,460] [DEBUG] DBManager initialized
|
||||
[2025-03-28 13:59:40,460] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 13:59:40,462] [DEBUG] AppManager.start() 호출됨 - 로그인 페이지 표시
|
||||
[2025-03-28 14:14:14,474] [DEBUG] Logger initialized
|
||||
[2025-03-28 14:15:19,450] [DEBUG] Logger initialized
|
||||
[2025-03-28 14:15:19,452] [DEBUG] Initializing DBManager...
|
||||
[2025-03-28 14:15:19,802] [DEBUG] DBManager initialized
|
||||
[2025-03-28 14:15:19,803] [DEBUG] AppManager.start() - 시작, 로그인 페이지 표시
|
||||
[2025-03-28 14:15:19,803] [DEBUG] Initializing LoginPage...
|
||||
[2025-03-28 14:15:33,083] [DEBUG] LoginPage.login() 호출됨
|
||||
[2025-03-28 14:15:33,084] [DEBUG] Entering DBManager.login()
|
||||
[2025-03-28 14:15:33,251] [DEBUG] Entering update_client_with_token()
|
||||
[2025-03-28 14:15:33,502] [DEBUG] Client updated with JWT token
|
||||
[2025-03-28 14:15:33,503] [DEBUG] Exiting update_client_with_token()
|
||||
[2025-03-28 14:15:33,503] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 14:15:33,503] [DEBUG] Exiting DBManager.login()
|
||||
[2025-03-28 14:15:33,504] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
|
||||
[2025-03-28 14:15:33,504] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
|
||||
[2025-03-28 14:15:33,891] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
|
||||
[2025-03-28 14:15:33,892] [DEBUG] Exiting update_last_login()
|
||||
[2025-03-28 14:15:33,894] [DEBUG] AppManager.on_login_success() 호출됨
|
||||
[2025-03-28 14:15:33,894] [DEBUG] AppManager.show_main_ui() 호출됨
|
||||
[2025-03-28 14:15:33,895] [DEBUG] Building MarketPage content
|
||||
[2025-03-28 14:15:33,896] [DEBUG] MarketPage.load_market_list() 호출됨
|
||||
[2025-03-28 14:15:33,896] [DEBUG] Entering DBManager.get_markets()
|
||||
[2025-03-28 14:15:33,962] [ERROR] get_markets 에러: {'code': '42P01', 'details': None, 'hint': None, 'message': 'relation "public.markets" does not exist'}
|
||||
Traceback (most recent call last):
|
||||
File "D:\py\Resell1\modules\db_manager.py", line 72, in get_markets
|
||||
response = self.client.table("markets").select("*").execute()
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\py\Resell1\.venv\Lib\site-packages\postgrest\_sync\request_builder.py", line 78, in execute
|
||||
raise APIError(r.json())
|
||||
postgrest.exceptions.APIError: {'code': '42P01', 'details': None, 'hint': None, 'message': 'relation "public.markets" does not exist'}
|
||||
|
||||
[2025-03-28 14:15:33,963] [DEBUG] Building ProductPage content
|
||||
[2025-03-28 14:15:33,964] [DEBUG] ForbiddenPage initialized
|
||||
[2025-03-28 14:15:33,965] [DEBUG] CategoryPage initialized
|
||||
|
|
|
|||
195
main.py
195
main.py
|
|
@ -1,196 +1,9 @@
|
|||
# main.py
|
||||
import flet as ft
|
||||
from modules import logger, login
|
||||
from modules.setting_manager import SettingsManager
|
||||
from modules.db_manager import DBManager
|
||||
from modules.main_window import MainWindow
|
||||
from modules.app_manager import AppManager
|
||||
|
||||
def main(page: ft.Page):
|
||||
page.title = "Modern Market and Product Manager"
|
||||
page.window_width = 1000
|
||||
page.window_height = 700
|
||||
app_manager = AppManager(page)
|
||||
app_manager.start()
|
||||
|
||||
# 하단 로그 출력용 텍스트 위젯
|
||||
log_display = ft.Text(value="", size=12)
|
||||
def gui_log_callback(formatted_message: str):
|
||||
print(formatted_message)
|
||||
|
||||
# 로거, 설정 관리자, DBManager 초기화
|
||||
app_logger = logger.get_logger(gui_callback=gui_log_callback)
|
||||
settings_manager = SettingsManager()
|
||||
db_manager = DBManager(app_logger)
|
||||
|
||||
# 로그인 다이얼로그 실행 (비동기)
|
||||
async def do_login():
|
||||
login_dialog = login.LoginDialog(page, app_logger, settings_manager, supabase_manager)
|
||||
logged_in = await login_dialog.show()
|
||||
if logged_in:
|
||||
page.controls.clear()
|
||||
page.add(ft.Text("로그인 성공! 메인 화면입니다."), log_display)
|
||||
else:
|
||||
page.add(ft.Text("로그인 실패!"), log_display)
|
||||
page.async_run(do_login)
|
||||
|
||||
# 마켓 탭 UI 구성
|
||||
market_tab_content = ft.Column([
|
||||
ft.Row([
|
||||
ft.ElevatedButton("마켓목록 가져오기", on_click=lambda e: load_market_list(page)),
|
||||
ft.ElevatedButton("팔린상품 가져오기", on_click=lambda e: load_sold_products(page)),
|
||||
ft.ElevatedButton("마켓추가하기", on_click=lambda e: add_market(page))
|
||||
]),
|
||||
ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("마켓이름")),
|
||||
ft.DataColumn(ft.Text("마켓 URL")),
|
||||
ft.DataColumn(ft.Text("메모"))
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="market_table"
|
||||
)
|
||||
], scroll=ft.ScrollMode.AUTO)
|
||||
|
||||
# 상품 탭 UI 구성
|
||||
product_tab_content = ft.Column([
|
||||
ft.Row([
|
||||
ft.ElevatedButton("금지어필터링", on_click=lambda e: filter_forbidden(page)),
|
||||
ft.ElevatedButton("카테고리 필터링", on_click=lambda e: filter_category(page)),
|
||||
ft.Dropdown(
|
||||
label="소싱몰 목록",
|
||||
options=[
|
||||
ft.dropdown.Option("타오바오"),
|
||||
ft.dropdown.Option("1688")
|
||||
],
|
||||
key="sourcing_market"
|
||||
),
|
||||
ft.ElevatedButton("소싱하기", on_click=lambda e: sourcing_products(page)),
|
||||
ft.ElevatedButton("출력", on_click=lambda e: export_products(page))
|
||||
]),
|
||||
ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("상품명")),
|
||||
ft.DataColumn(ft.Text("카테고리")),
|
||||
ft.DataColumn(ft.Text("이미지 URL")),
|
||||
ft.DataColumn(ft.Text("소싱 URL"))
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="product_table"
|
||||
)
|
||||
], scroll=ft.ScrollMode.AUTO)
|
||||
|
||||
# 금지어 관리 탭 (추후 구현)
|
||||
forbidden_tab_content = ft.Column([
|
||||
ft.Text("금지어 관리 탭 내용 (추후 구현)")
|
||||
])
|
||||
|
||||
# 카테고리 관리 탭 (추후 구현)
|
||||
category_tab_content = ft.Column([
|
||||
ft.Text("카테고리 관리 탭 내용 (추후 구현)")
|
||||
])
|
||||
|
||||
# 메인 탭 생성
|
||||
tabs = ft.Tabs(
|
||||
selected_index=0,
|
||||
tabs=[
|
||||
ft.Tab(text="마켓", content=market_tab_content),
|
||||
ft.Tab(text="상품", content=product_tab_content),
|
||||
ft.Tab(text="금지어 관리", content=forbidden_tab_content),
|
||||
ft.Tab(text="카테고리 관리", content=category_tab_content)
|
||||
],
|
||||
key="main_tabs"
|
||||
)
|
||||
|
||||
# 페이지 레이아웃 구성
|
||||
page.add(tabs, log_display)
|
||||
|
||||
# 로그 추가 함수 (각 모듈에서 호출 가능하도록 page.session에 저장)
|
||||
def append_log(message: str):
|
||||
current = log_display.value
|
||||
log_display.value = current + message + "\n"
|
||||
page.update()
|
||||
page.session.set("append_log", append_log)
|
||||
|
||||
def load_market_list(page: ft.Page):
|
||||
global market_list
|
||||
page.session.get("append_log")("Fetching market list...")
|
||||
market_list = backend.get_market_list()
|
||||
market_rows = []
|
||||
for m in market_list:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(m.get("name", ""))),
|
||||
ft.DataCell(ft.Text(m.get("url", ""))),
|
||||
ft.DataCell(ft.Text(m.get("memo", "")))
|
||||
])
|
||||
market_rows.append(row)
|
||||
market_table: ft.DataTable = page.get_control("market_table")
|
||||
market_table.rows = market_rows
|
||||
page.session.get("append_log")("Market list loaded.")
|
||||
page.update()
|
||||
|
||||
def load_sold_products(page: ft.Page):
|
||||
global sold_products, filtered_products, sourced_products
|
||||
page.session.get("append_log")("Fetching sold products for each market...")
|
||||
sold_products = backend.get_sold_products(market_list)
|
||||
filtered_products = sold_products.copy()
|
||||
page.session.get("append_log")("Sold products loaded. Switching to 상품 탭.")
|
||||
update_product_table(page, filtered_products)
|
||||
tabs: ft.Tabs = page.get_control("main_tabs")
|
||||
tabs.selected_index = 1
|
||||
page.update()
|
||||
|
||||
def update_product_table(page: ft.Page, products):
|
||||
product_rows = []
|
||||
for p in products:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(p.get("name", ""))),
|
||||
ft.DataCell(ft.Text(p.get("category", ""))),
|
||||
ft.DataCell(ft.Text(p.get("image_url", ""))),
|
||||
ft.DataCell(ft.Text(p.get("sourcing_url", "")))
|
||||
])
|
||||
product_rows.append(row)
|
||||
product_table: ft.DataTable = page.get_control("product_table")
|
||||
product_table.rows = product_rows
|
||||
page.update()
|
||||
|
||||
def add_market(page: ft.Page):
|
||||
page.session.get("append_log")("Add market functionality not implemented yet.")
|
||||
page.update()
|
||||
|
||||
def filter_forbidden(page: ft.Page):
|
||||
global filtered_products
|
||||
page.session.get("append_log")("Filtering products with forbidden words...")
|
||||
filtered_products = product_filter.filter_forbidden_words(filtered_products)
|
||||
update_product_table(page, filtered_products)
|
||||
page.session.get("append_log")("Forbidden words filtering applied.")
|
||||
page.update()
|
||||
|
||||
def filter_category(page: ft.Page):
|
||||
global filtered_products
|
||||
page.session.get("append_log")("Filtering products with forbidden categories...")
|
||||
filtered_products = product_filter.filter_forbidden_categories(filtered_products)
|
||||
update_product_table(page, filtered_products)
|
||||
page.session.get("append_log")("Category filtering applied.")
|
||||
page.update()
|
||||
|
||||
def sourcing_products(page: ft.Page):
|
||||
global sourced_products, filtered_products
|
||||
sourcing_market: ft.Dropdown = page.get_control("sourcing_market")
|
||||
selected_market = sourcing_market.value
|
||||
page.session.get("append_log")(f"Starting sourcing using {selected_market}...")
|
||||
sourced_products = []
|
||||
for product in filtered_products:
|
||||
sourcing_url = backend.sourcing_product(product.get("image_url", ""), selected_market)
|
||||
product["sourcing_url"] = sourcing_url
|
||||
sourced_products.append(product)
|
||||
update_product_table(page, sourced_products)
|
||||
page.session.get("append_log")("Sourcing completed.")
|
||||
page.update()
|
||||
|
||||
def export_products(page: ft.Page):
|
||||
page.session.get("append_log")("Exporting products to Excel...")
|
||||
export.export_to_excel(sourced_products)
|
||||
page.session.get("append_log")("Products exported and folder opened.")
|
||||
page.update()
|
||||
|
||||
if __name__ == "__main__":
|
||||
ft.app(target=main)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,106 @@
|
|||
# modules/app_manager.py
|
||||
import flet as ft
|
||||
import logging
|
||||
from modules import logger, setting_manager, db_manager, project_info, login_page, market_page, product_page, forbidden_page, category_page, app_state
|
||||
|
||||
class AppManager:
|
||||
def __init__(self, page: ft.Page):
|
||||
self.page = page
|
||||
self.logger = logger.get_logger()
|
||||
self.project_info = project_info.get_project_info()
|
||||
# 창 제목은 project_info의 window_title 사용
|
||||
self.page.title = self.project_info.get("window_title", "Modern Market Manager")
|
||||
self.page.theme_mode = ft.ThemeMode.LIGHT
|
||||
|
||||
# 앱 상태 관리 객체
|
||||
self.state = app_state.AppState()
|
||||
|
||||
# 초기 관리자 객체 생성
|
||||
self.settings_mgr = setting_manager.SettingsManager(self.project_info)
|
||||
self.db_mgr = db_manager.DBManager(self.logger)
|
||||
|
||||
def start(self):
|
||||
self.logger.debug("AppManager.start() - 시작, 로그인 페이지 표시")
|
||||
from modules.login_page import LoginPage
|
||||
login_pg = LoginPage(
|
||||
self.page,
|
||||
self.logger,
|
||||
self.settings_mgr,
|
||||
self.db_mgr,
|
||||
on_login_success=self.on_login_success,
|
||||
project_info=self.project_info
|
||||
)
|
||||
login_pg.show()
|
||||
|
||||
def on_login_success(self):
|
||||
self.logger.debug("AppManager.on_login_success() 호출됨")
|
||||
# 예를 들어, 로그인 성공 시 AppState에 사용자 정보를 저장할 수 있음.
|
||||
# 전환 후 메인 UI (탭 + 로그 영역) 표시
|
||||
self.show_main_ui()
|
||||
|
||||
def show_main_ui(self):
|
||||
self.logger.debug("AppManager.show_main_ui() 호출됨")
|
||||
self.page.controls.clear()
|
||||
|
||||
# 각 페이지 인스턴스 생성
|
||||
from modules.market_page import MarketPage
|
||||
from modules.product_page import ProductPage
|
||||
from modules.forbidden_page import ForbiddenPage
|
||||
from modules.category_page import CategoryPage
|
||||
|
||||
market_pg = MarketPage(self.page, self.logger, self.db_mgr)
|
||||
product_pg = ProductPage(self.page, self.logger)
|
||||
forbidden_pg = ForbiddenPage(self.page, self.logger)
|
||||
category_pg = CategoryPage(self.page, self.logger)
|
||||
|
||||
# "팔린상품 가져오기" 버튼 클릭 시 호출될 콜백 (상품 페이지 업데이트 및 탭 전환)
|
||||
def on_products_fetched(products):
|
||||
self.logger.debug(f"AppManager.on_products_fetched() 호출됨, 상품 수: {len(products)}")
|
||||
product_pg.set_products(products)
|
||||
tabs.selected_index = 1 # 두 번째 탭으로 전환 (상품 페이지)
|
||||
self.page.update()
|
||||
|
||||
market_pg.on_products_fetched_callback = on_products_fetched
|
||||
|
||||
# 탭 컨트롤 구성
|
||||
tabs = ft.Tabs(
|
||||
selected_index=0,
|
||||
tabs=[
|
||||
ft.Tab(text="마켓", content=market_pg.get_content()),
|
||||
ft.Tab(text="상품", content=product_pg.get_content()),
|
||||
ft.Tab(text="금지어 관리", content=forbidden_pg.get_content()),
|
||||
ft.Tab(text="카테고리 관리", content=category_pg.get_content()),
|
||||
],
|
||||
on_change=self.on_tab_change
|
||||
)
|
||||
|
||||
# 로그 영역: 스크롤 가능한 컨테이너에 Text 컨트롤 배치
|
||||
log_text = ft.Text("", size=12, color="black")
|
||||
log_container = ft.Container(
|
||||
content=log_text,
|
||||
height=150,
|
||||
scroll=ft.ScrollMode.AUTO,
|
||||
border=ft.border.all(1, "lightgray"),
|
||||
padding=10,
|
||||
bgcolor="white",
|
||||
)
|
||||
|
||||
main_layout = ft.Column([tabs, log_container], expand=True, spacing=10)
|
||||
|
||||
# 창 크기 및 중앙 배치 (1400×800)
|
||||
self.page.window.width = 1400
|
||||
self.page.window.height = 800
|
||||
self.page.window.center()
|
||||
|
||||
self.page.add(main_layout)
|
||||
self.page.update()
|
||||
|
||||
# Logger의 GUI 콜백 설정하여 로그가 log_text에 출력되도록 함
|
||||
def gui_log_callback(message: str):
|
||||
log_text.value += message + "\n"
|
||||
self.page.update()
|
||||
self.logger.gui_callback = gui_log_callback
|
||||
|
||||
def on_tab_change(self, e: ft.ControlEvent):
|
||||
self.logger.debug(f"탭 전환됨, 인덱스: {e.control.selected_index}")
|
||||
self.page.update()
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# modules/app_state.py
|
||||
class AppState:
|
||||
def __init__(self):
|
||||
self.user_info = {}
|
||||
self.current_page = "login" # "login", "market", "product", "forbidden", "category"
|
||||
|
||||
def set_user_info(self, info: dict):
|
||||
self.user_info = info
|
||||
|
||||
def get_user_info(self) -> dict:
|
||||
return self.user_info
|
||||
|
||||
def set_current_page(self, page_name: str):
|
||||
self.current_page = page_name
|
||||
|
||||
def get_current_page(self) -> str:
|
||||
return self.current_page
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# modules/category_page.py
|
||||
import flet as ft
|
||||
import logging
|
||||
|
||||
class CategoryPage:
|
||||
def __init__(self, page: ft.Page, logger: logging.Logger):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.logger.debug("CategoryPage initialized")
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
self.header = ft.Text("카테고리 관리 페이지", style=ft.TextStyle(size=24, weight="bold"), color="black")
|
||||
self.info = ft.Text("카테고리 목록 및 관리를 여기에 표시합니다.", color="black")
|
||||
self.content = ft.Column([self.header, self.info], spacing=10)
|
||||
|
||||
def get_content(self):
|
||||
return self.content
|
||||
|
|
@ -6,10 +6,6 @@ import logging
|
|||
import traceback
|
||||
|
||||
class DBManager:
|
||||
"""
|
||||
DBManager는 Supabase 클라이언트를 래핑하여 로그인, 사용자 정보 조회,
|
||||
마지막 로그인 시간 업데이트 등 여러 API 호출을 수행합니다.
|
||||
"""
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
self.logger.log("Initializing DBManager...", level=logging.DEBUG)
|
||||
|
|
@ -57,38 +53,6 @@ class DBManager:
|
|||
self.logger.log(f"Login error: {e}", level=logging.ERROR, exc_info=True)
|
||||
return {"error": str(e)}
|
||||
|
||||
def get_auth_user_info(self, user_id: str) -> dict:
|
||||
self.logger.log(f"Entering get_auth_user_info(user_id={user_id})", level=logging.DEBUG)
|
||||
try:
|
||||
response = self.client.from_("users").select("email_confirmed_at").eq("id", user_id).execute()
|
||||
if response.data and len(response.data) > 0:
|
||||
self.logger.log(f"Retrieved auth user info: {response.data[0]}", level=logging.DEBUG)
|
||||
return response.data[0]
|
||||
else:
|
||||
self.logger.log("auth.users에서 사용자 정보를 찾지 못했습니다.", level=logging.WARNING)
|
||||
return {}
|
||||
except Exception as e:
|
||||
self.logger.log(f"get_auth_user_info 에러: {e}", level=logging.ERROR, exc_info=True)
|
||||
return {}
|
||||
|
||||
def get_full_user_info(self, user_id: str) -> dict:
|
||||
self.logger.log(f"Entering get_full_user_info(user_id={user_id})", level=logging.DEBUG)
|
||||
try:
|
||||
user_resp = self.client.table("users").select("*").eq("id", user_id).execute()
|
||||
if not user_resp.data:
|
||||
self.logger.log("사용자 정보가 존재하지 않습니다.", level=logging.WARNING)
|
||||
return {}
|
||||
user_info = user_resp.data[0]
|
||||
membership_level = user_info.get("membership_level", "default")
|
||||
membership_resp = self.client.table("membership_levels").select("*").eq("level", membership_level).execute()
|
||||
membership_info = membership_resp.data[0] if membership_resp.data else {}
|
||||
full_info = {**user_info, "membership_level_data": membership_info}
|
||||
self.logger.log(f"Full user info retrieved: {full_info}", level=logging.DEBUG)
|
||||
return full_info
|
||||
except Exception as e:
|
||||
self.logger.log(f"get_full_user_info 에러: {e}", level=logging.ERROR, exc_info=True)
|
||||
return {}
|
||||
|
||||
def update_last_login(self, user_id: str):
|
||||
self.logger.log(f"Entering update_last_login(user_id={user_id})", level=logging.DEBUG)
|
||||
try:
|
||||
|
|
@ -98,3 +62,17 @@ class DBManager:
|
|||
except Exception as e:
|
||||
self.logger.log(f"update_last_login 에러: {e}", level=logging.ERROR, exc_info=True)
|
||||
self.logger.log("Exiting update_last_login()", level=logging.DEBUG)
|
||||
|
||||
def get_markets(self) -> list:
|
||||
"""
|
||||
supabase의 'markets' 테이블에서 마켓 목록을 읽어와 반환합니다.
|
||||
"""
|
||||
self.logger.log("Entering DBManager.get_markets()", level=logging.DEBUG)
|
||||
try:
|
||||
response = self.client.table("markets").select("*").execute()
|
||||
markets = response.data if response.data else []
|
||||
self.logger.log(f"Retrieved markets: {markets}", level=logging.DEBUG)
|
||||
return markets
|
||||
except Exception as e:
|
||||
self.logger.log(f"get_markets 에러: {e}", level=logging.ERROR, exc_info=True)
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# modules/forbidden_page.py
|
||||
import flet as ft
|
||||
import logging
|
||||
|
||||
class ForbiddenPage:
|
||||
def __init__(self, page: ft.Page, logger: logging.Logger):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.logger.debug("ForbiddenPage initialized")
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
self.header = ft.Text("금지어 관리 페이지", style=ft.TextStyle(size=24, weight="bold"), color="black")
|
||||
self.info = ft.Text("금지어 목록 및 관리를 여기에 표시합니다.", color="black")
|
||||
self.content = ft.Column([self.header, self.info], spacing=10)
|
||||
|
||||
def get_content(self):
|
||||
return self.content
|
||||
|
|
@ -5,47 +5,44 @@ import traceback
|
|||
class Logger:
|
||||
def __init__(self, gui_callback=None, log_file="app.log", logger_name="FletLogger", level=logging.DEBUG):
|
||||
"""
|
||||
gui_callback: GUI에 로그 메시지를 전달하는 콜백 함수 (매개변수: formatted_message:str)
|
||||
gui_callback: GUI에 로그 메시지를 전달하는 콜백 함수 (여기서는 단순 텍스트 메시지)
|
||||
"""
|
||||
self.gui_callback = gui_callback
|
||||
self.logger = logging.getLogger(logger_name)
|
||||
self.logger.setLevel(level)
|
||||
# 파일 핸들러 설정 (RotatingFileHandler)
|
||||
# 파일 핸들러 (최대 10MB, 5회 백업)
|
||||
file_handler = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5, encoding="utf-8")
|
||||
file_handler.setLevel(level)
|
||||
formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
|
||||
file_handler.setFormatter(formatter)
|
||||
self.logger.addHandler(file_handler)
|
||||
# 콘솔 핸들러 설정
|
||||
# 콘솔 핸들러
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(level)
|
||||
console_handler.setFormatter(formatter)
|
||||
self.logger.addHandler(console_handler)
|
||||
self.log("Logger initialized", level=logging.DEBUG)
|
||||
|
||||
def log(self, message, level=logging.INFO, exc_info=False):
|
||||
"""
|
||||
메시지를 기록하고, GUI 콜백이 있다면 레벨별 색상이 적용된 HTML 문자열을 전달합니다.
|
||||
"""
|
||||
if exc_info:
|
||||
message += "\n" + traceback.format_exc()
|
||||
self.logger.log(level, message)
|
||||
if self.gui_callback:
|
||||
formatted_message = self.format_gui_message(message, level)
|
||||
self.gui_callback(formatted_message)
|
||||
# HTML 태그 없이 순수 텍스트를 전달합니다.
|
||||
self.gui_callback(message)
|
||||
|
||||
def format_gui_message(self, message, level):
|
||||
"""
|
||||
레벨별 색상을 적용한 HTML 문자열 반환.
|
||||
"""
|
||||
color_map = {
|
||||
logging.DEBUG: "gray",
|
||||
logging.INFO: "black",
|
||||
logging.WARNING: "orange",
|
||||
logging.ERROR: "red",
|
||||
logging.CRITICAL: "purple"
|
||||
}
|
||||
color = color_map.get(level, "black")
|
||||
return f'<span style="color:{color};">{message}</span>'
|
||||
# 편의 메서드 추가
|
||||
def debug(self, message):
|
||||
self.log(message, level=logging.DEBUG)
|
||||
|
||||
def info(self, message):
|
||||
self.log(message, level=logging.INFO)
|
||||
|
||||
def warning(self, message):
|
||||
self.log(message, level=logging.WARNING)
|
||||
|
||||
def error(self, message):
|
||||
self.log(message, level=logging.ERROR)
|
||||
|
||||
def get_logger(gui_callback=None):
|
||||
return Logger(gui_callback=gui_callback)
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
import flet as ft
|
||||
from flet import AlertDialog, TextField, Checkbox, ElevatedButton, Text, Column, Row, MainAxisAlignment
|
||||
import logging
|
||||
|
||||
class LoginDialog:
|
||||
"""
|
||||
flet 기반 로그인 다이얼로그.
|
||||
|
||||
- 이메일, 비밀번호 입력란
|
||||
- "정보 저장" 체크박스 (체크 시, SettingsManager를 통해 사용자 정보를 저장)
|
||||
- "비밀번호 보기" 체크박스 (비밀번호 입력란 에코 모드 전환)
|
||||
- 로그인 및 비밀번호 찾기 버튼
|
||||
"""
|
||||
def __init__(self, page: ft.Page, logger: logging.Logger, settings_manager, supabase_manager):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.settings_manager = settings_manager
|
||||
self.supabase_manager = supabase_manager
|
||||
self.result = None # 로그인 성공 여부 (True/False)
|
||||
self.create_dialog()
|
||||
|
||||
def create_dialog(self):
|
||||
self.logger.log("Entering LoginDialog.create_dialog()", level=logging.DEBUG)
|
||||
self.email_field = TextField(label="이메일", width=300)
|
||||
self.password_field = TextField(label="비밀번호", width=300, password=True)
|
||||
self.remember_checkbox = Checkbox(label="정보 저장")
|
||||
self.show_password_checkbox = Checkbox(label="비밀번호 보기")
|
||||
self.error_text = Text("", color="red")
|
||||
|
||||
self.show_password_checkbox.on_change = self.toggle_password
|
||||
|
||||
self.login_button = ElevatedButton("로그인", on_click=self.on_login)
|
||||
self.reset_button = ElevatedButton("비밀번호 찾기", on_click=self.on_reset)
|
||||
|
||||
content = Column([
|
||||
self.email_field,
|
||||
self.password_field,
|
||||
Row([self.remember_checkbox, self.show_password_checkbox]),
|
||||
self.error_text,
|
||||
Row([self.login_button, self.reset_button], alignment=MainAxisAlignment.CENTER)
|
||||
])
|
||||
|
||||
self.dialog = AlertDialog(
|
||||
title=Text("로그인"),
|
||||
content=content,
|
||||
actions_alignment=MainAxisAlignment.CENTER
|
||||
)
|
||||
self.page.dialog = self.dialog
|
||||
self.dialog.open = True
|
||||
self.page.update()
|
||||
|
||||
def toggle_password(self, e: ft.ControlEvent):
|
||||
self.logger.log("toggle_password() 호출됨", level=logging.DEBUG)
|
||||
self.password_field.password = not self.show_password_checkbox.value
|
||||
self.page.update()
|
||||
|
||||
def on_login(self, e: ft.ControlEvent):
|
||||
self.logger.log("on_login() 호출됨", level=logging.DEBUG)
|
||||
email = self.email_field.value.strip()
|
||||
password = self.password_field.value.strip()
|
||||
if not email or not password:
|
||||
self.error_text.value = "이메일과 비밀번호를 모두 입력하세요."
|
||||
self.logger.log("on_login(): 입력값 부족", level=logging.WARNING)
|
||||
self.page.update()
|
||||
return
|
||||
|
||||
result = self.db_manager.login(email, password)
|
||||
if "error" not in result:
|
||||
self.logger.log(f"로그인 성공: {result}", level=logging.DEBUG)
|
||||
self.db_manager.update_last_login(result["id"])
|
||||
if self.remember_checkbox.value:
|
||||
user_info = {"email": email, "password": password, "id": result["id"]}
|
||||
self.settings_manager.save_user_info(user_info)
|
||||
self.result = True
|
||||
self.dialog.open = False
|
||||
self.page.dialog = None
|
||||
self.page.update()
|
||||
else:
|
||||
self.error_text.value = f"로그인 실패: {result['error']}"
|
||||
self.logger.log(f"로그인 실패: {result['error']}", level=logging.WARNING)
|
||||
self.page.update()
|
||||
|
||||
def on_reset(self, e: ft.ControlEvent):
|
||||
self.logger.log("on_reset() 호출됨", level=logging.DEBUG)
|
||||
self.error_text.value = "비밀번호 찾기 기능은 구현되지 않았습니다."
|
||||
self.page.update()
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# modules/login_page.py
|
||||
import flet as ft
|
||||
import logging
|
||||
import tkinter as tk
|
||||
|
||||
class LoginPage:
|
||||
def __init__(self, page: ft.Page, logger: logging.Logger, settings_manager, db_manager, on_login_success, project_info):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.settings_manager = settings_manager
|
||||
self.db_manager = db_manager
|
||||
self.on_login_success = on_login_success
|
||||
self.project_info = project_info # {'name': ..., 'window_title': ..., 'version': ..., 'authors': ...}
|
||||
self.logger.log("Initializing LoginPage...", level=logging.DEBUG)
|
||||
self.build_page()
|
||||
|
||||
def build_page(self):
|
||||
# 헤더: 프로그램 제목, 버전, 제작자 정보
|
||||
self.header = ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
ft.Text(self.project_info.get("window_title", "App"), style=ft.TextStyle(size=30, weight="bold"), color="black"),
|
||||
ft.Text(f"Version {self.project_info.get('version', '0.0.0')}", style=ft.TextStyle(size=16), color="gray"),
|
||||
ft.Text(f"제작자: {self.project_info.get('authors', '')}", style=ft.TextStyle(size=16), color="gray"),
|
||||
],
|
||||
horizontal_alignment="center",
|
||||
spacing=5,
|
||||
),
|
||||
padding=10,
|
||||
bgcolor="white",
|
||||
alignment=ft.alignment.center,
|
||||
border=ft.border.all(1, "lightgray"),
|
||||
border_radius=ft.border_radius.all(5),
|
||||
)
|
||||
|
||||
# 로그인 폼
|
||||
self.username_field = ft.TextField(
|
||||
label="사용자 이름", width=300, on_submit=lambda e: self.password_field.focus()
|
||||
)
|
||||
self.password_field = ft.TextField(
|
||||
label="비밀번호", width=300, password=True, on_submit=lambda e: self.login(None)
|
||||
)
|
||||
self.remember_checkbox = ft.Checkbox(label="정보 저장")
|
||||
self.login_button = ft.ElevatedButton(text="로그인", on_click=self.login)
|
||||
self.message = ft.Text("", color="red")
|
||||
|
||||
# 로그인 폼 컨테이너
|
||||
self.form_container = ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
self.username_field,
|
||||
self.password_field,
|
||||
self.remember_checkbox,
|
||||
self.login_button,
|
||||
self.message,
|
||||
],
|
||||
alignment="center",
|
||||
horizontal_alignment="center",
|
||||
spacing=15,
|
||||
),
|
||||
padding=20,
|
||||
bgcolor="white",
|
||||
border=ft.border.all(1, "lightgray"),
|
||||
border_radius=ft.border_radius.all(10),
|
||||
shadow=ft.BoxShadow(blur_radius=10, spread_radius=1, offset=ft.Offset(2,2), color="gray"),
|
||||
)
|
||||
|
||||
# 전체 로그인 페이지 컨텐츠 (중앙 정렬)
|
||||
self.controls = [
|
||||
ft.Container(
|
||||
content=ft.Column(
|
||||
[
|
||||
self.header,
|
||||
self.form_container,
|
||||
],
|
||||
spacing=20,
|
||||
horizontal_alignment="center",
|
||||
),
|
||||
alignment=ft.alignment.center,
|
||||
expand=True,
|
||||
bgcolor="#f0f2f5",
|
||||
)
|
||||
]
|
||||
|
||||
def login(self, e):
|
||||
self.logger.log("LoginPage.login() 호출됨", level=logging.DEBUG)
|
||||
username = self.username_field.value.strip()
|
||||
password = self.password_field.value.strip()
|
||||
if not username or not password:
|
||||
self.message.value = "사용자 이름과 비밀번호를 모두 입력하세요."
|
||||
self.page.update()
|
||||
return
|
||||
|
||||
result = self.db_manager.login(username, password)
|
||||
if "error" not in result:
|
||||
self.logger.log(f"로그인 성공: {result}", level=logging.DEBUG)
|
||||
self.db_manager.update_last_login(result["id"])
|
||||
if self.remember_checkbox.value:
|
||||
user_info = {"username": username, "password": password, "id": result["id"]}
|
||||
self.settings_manager.save_user_info(user_info)
|
||||
self.message.value = "로그인 성공!"
|
||||
self.page.update()
|
||||
self.on_login_success()
|
||||
else:
|
||||
self.message.value = f"로그인 실패: {result['error']}"
|
||||
self.logger.log(f"로그인 실패: {result['error']}", level=logging.WARNING)
|
||||
self.page.update()
|
||||
|
||||
def show(self):
|
||||
# 창 크기를 500×500로 설정하고 중앙 배치
|
||||
self.page.window.width = 500
|
||||
self.page.window.height = 500
|
||||
# 중앙 배치를 위해 tkinter로 화면 해상도 계산
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
screen_width = root.winfo_screenwidth()
|
||||
screen_height = root.winfo_screenheight()
|
||||
root.destroy()
|
||||
self.page.window.x = int((screen_width - 500) / 2)
|
||||
self.page.window.y = int((screen_height - 500) / 2)
|
||||
if hasattr(self.page.window, "center"):
|
||||
self.page.window.center()
|
||||
self.page.controls.clear()
|
||||
for ctrl in self.controls:
|
||||
self.page.add(ctrl)
|
||||
self.page.update()
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
import flet as ft
|
||||
import logging
|
||||
from modules import backend, product_filter, export
|
||||
|
||||
class MainWindow:
|
||||
def __init__(self, page: ft.Page):
|
||||
self.page = page
|
||||
self.logger = logging.getLogger("FletLogger")
|
||||
self.logger.debug("MainWindow initialized")
|
||||
self.market_list = []
|
||||
self.sold_products = []
|
||||
self.filtered_products = []
|
||||
self.sourced_products = []
|
||||
self.controls = self.build_layout()
|
||||
|
||||
def build_layout(self):
|
||||
self.logger.debug("Building main window layout")
|
||||
<<<<<<< HEAD
|
||||
# 마켓 탭
|
||||
=======
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
self.market_table = ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("마켓이름")),
|
||||
ft.DataColumn(ft.Text("마켓 URL")),
|
||||
ft.DataColumn(ft.Text("메모"))
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="market_table"
|
||||
)
|
||||
market_tab_content = ft.Column([
|
||||
ft.Row([
|
||||
ft.ElevatedButton("마켓목록 가져오기", on_click=self.load_market_list),
|
||||
ft.ElevatedButton("팔린상품 가져오기", on_click=self.load_sold_products),
|
||||
ft.ElevatedButton("마켓추가하기", on_click=self.add_market)
|
||||
]),
|
||||
self.market_table
|
||||
], scroll=ft.ScrollMode.AUTO)
|
||||
|
||||
<<<<<<< HEAD
|
||||
# 상품 탭
|
||||
=======
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
self.product_table = ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("상품명")),
|
||||
ft.DataColumn(ft.Text("카테고리")),
|
||||
ft.DataColumn(ft.Text("이미지 URL")),
|
||||
ft.DataColumn(ft.Text("소싱 URL"))
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="product_table"
|
||||
)
|
||||
self.sourcing_market_dropdown = ft.Dropdown(
|
||||
label="소싱몰 목록",
|
||||
options=[
|
||||
ft.dropdown.Option("타오바오"),
|
||||
ft.dropdown.Option("1688")
|
||||
],
|
||||
key="sourcing_market"
|
||||
)
|
||||
product_tab_content = ft.Column([
|
||||
ft.Row([
|
||||
ft.ElevatedButton("금지어필터링", on_click=self.filter_forbidden),
|
||||
ft.ElevatedButton("카테고리 필터링", on_click=self.filter_category),
|
||||
self.sourcing_market_dropdown,
|
||||
ft.ElevatedButton("소싱하기", on_click=self.sourcing_products),
|
||||
ft.ElevatedButton("출력", on_click=self.export_products)
|
||||
]),
|
||||
self.product_table
|
||||
], scroll=ft.ScrollMode.AUTO)
|
||||
|
||||
<<<<<<< HEAD
|
||||
# 기타 탭
|
||||
=======
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
forbidden_tab_content = ft.Column([ft.Text("금지어 관리 탭 (추후 구현)")])
|
||||
category_tab_content = ft.Column([ft.Text("카테고리 관리 탭 (추후 구현)")])
|
||||
|
||||
self.tabs = ft.Tabs(
|
||||
selected_index=0,
|
||||
tabs=[
|
||||
ft.Tab(text="마켓", content=market_tab_content),
|
||||
ft.Tab(text="상품", content=product_tab_content),
|
||||
ft.Tab(text="금지어 관리", content=forbidden_tab_content),
|
||||
ft.Tab(text="카테고리 관리", content=category_tab_content)
|
||||
],
|
||||
key="main_tabs"
|
||||
)
|
||||
|
||||
<<<<<<< HEAD
|
||||
# 로그 출력
|
||||
=======
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
self.log_display = ft.Text(value="", size=12)
|
||||
def append_log(message: str):
|
||||
self.log_display.value += message + "\n"
|
||||
self.page.update()
|
||||
self.page.session.set("append_log", append_log)
|
||||
|
||||
layout = [self.tabs, self.log_display]
|
||||
self.logger.debug("Main window layout built")
|
||||
return layout
|
||||
|
||||
<<<<<<< HEAD
|
||||
# 이하 메서드들은 모두 디버그 로깅 + 기능 수행
|
||||
=======
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
def load_market_list(self, e):
|
||||
self.logger.debug("load_market_list() 호출됨")
|
||||
self.page.session.get("append_log")("Fetching market list...")
|
||||
self.market_list = backend.get_market_list()
|
||||
rows = []
|
||||
for m in self.market_list:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(m.get("name", ""))),
|
||||
ft.DataCell(ft.Text(m.get("url", ""))),
|
||||
ft.DataCell(ft.Text(m.get("memo", "")))
|
||||
])
|
||||
rows.append(row)
|
||||
self.market_table.rows = rows
|
||||
self.page.session.get("append_log")("Market list loaded.")
|
||||
self.page.update()
|
||||
|
||||
def load_sold_products(self, e):
|
||||
self.logger.debug("load_sold_products() 호출됨")
|
||||
self.page.session.get("append_log")("Fetching sold products for each market...")
|
||||
self.sold_products = backend.get_sold_products(self.market_list)
|
||||
self.filtered_products = self.sold_products.copy()
|
||||
self.page.session.get("append_log")("Sold products loaded. Switching to 상품 탭.")
|
||||
self.update_product_table(self.filtered_products)
|
||||
self.tabs.selected_index = 1
|
||||
self.page.update()
|
||||
|
||||
def update_product_table(self, products):
|
||||
self.logger.debug("update_product_table() 호출됨")
|
||||
rows = []
|
||||
for p in products:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(p.get("name", ""))),
|
||||
ft.DataCell(ft.Text(p.get("category", ""))),
|
||||
ft.DataCell(ft.Text(p.get("image_url", ""))),
|
||||
ft.DataCell(ft.Text(p.get("sourcing_url", "")))
|
||||
])
|
||||
rows.append(row)
|
||||
self.product_table.rows = rows
|
||||
self.page.update()
|
||||
|
||||
def add_market(self, e):
|
||||
self.logger.debug("add_market() 호출됨")
|
||||
self.page.session.get("append_log")("Add market functionality not implemented yet.")
|
||||
self.page.update()
|
||||
|
||||
def filter_forbidden(self, e):
|
||||
self.logger.debug("filter_forbidden() 호출됨")
|
||||
self.page.session.get("append_log")("Filtering products with forbidden words...")
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
from modules import product_filter
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
self.filtered_products = product_filter.filter_forbidden_words(self.filtered_products)
|
||||
self.update_product_table(self.filtered_products)
|
||||
self.page.session.get("append_log")("Forbidden words filtering applied.")
|
||||
self.page.update()
|
||||
|
||||
def filter_category(self, e):
|
||||
self.logger.debug("filter_category() 호출됨")
|
||||
self.page.session.get("append_log")("Filtering products with forbidden categories...")
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
from modules import product_filter
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
self.filtered_products = product_filter.filter_forbidden_categories(self.filtered_products)
|
||||
self.update_product_table(self.filtered_products)
|
||||
self.page.session.get("append_log")("Category filtering applied.")
|
||||
self.page.update()
|
||||
|
||||
def sourcing_products(self, e):
|
||||
self.logger.debug("sourcing_products() 호출됨")
|
||||
<<<<<<< HEAD
|
||||
self
|
||||
=======
|
||||
self.page.session.get("append_log")(f"Starting sourcing using {self.sourcing_market_dropdown.value}...")
|
||||
from modules import backend
|
||||
self.sourced_products = []
|
||||
for product in self.filtered_products:
|
||||
url = backend.sourcing_product(product.get("image_url", ""), self.sourcing_market_dropdown.value)
|
||||
product["sourcing_url"] = url
|
||||
self.sourced_products.append(product)
|
||||
self.update_product_table(self.sourced_products)
|
||||
self.page.session.get("append_log")("Sourcing completed.")
|
||||
self.page.update()
|
||||
|
||||
def export_products(self, e):
|
||||
self.logger.debug("export_products() 호출됨")
|
||||
self.page.session.get("append_log")("Exporting products to Excel...")
|
||||
from modules import export
|
||||
export.export_to_excel(self.sourced_products)
|
||||
self.page.session.get("append_log")("Products exported and folder opened.")
|
||||
self.page.update()
|
||||
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
# modules/market_page.py
|
||||
import flet as ft
|
||||
import logging
|
||||
from modules import db_manager
|
||||
|
||||
class MarketPage:
|
||||
def __init__(self, page: ft.Page, logger: logging.Logger, db_manager):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.db_manager = db_manager
|
||||
self.market_list = []
|
||||
self.on_products_fetched_callback = None # 외부에서 설정 (ProductPage 업데이트용)
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
self.logger.debug("Building MarketPage content")
|
||||
# 버튼들
|
||||
self.load_market_btn = ft.ElevatedButton("마켓목록 가져오기", on_click=self.load_market_list)
|
||||
self.load_sold_products_btn = ft.ElevatedButton("팔린상품 가져오기", on_click=self.load_sold_products)
|
||||
self.add_market_btn = ft.ElevatedButton("마켓추가하기", on_click=self.add_market)
|
||||
# 마켓 목록 테이블
|
||||
self.market_table = ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("마켓이름")),
|
||||
ft.DataColumn(ft.Text("마켓 URL")),
|
||||
ft.DataColumn(ft.Text("메모"))
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="market_table"
|
||||
)
|
||||
self.content = ft.Column(
|
||||
[
|
||||
ft.Row([self.load_market_btn, self.load_sold_products_btn, self.add_market_btn]),
|
||||
self.market_table
|
||||
],
|
||||
spacing=10,
|
||||
)
|
||||
self.load_market_list(None)
|
||||
|
||||
def load_market_list(self, e):
|
||||
self.logger.debug("MarketPage.load_market_list() 호출됨")
|
||||
self.market_list = self.db_manager.get_markets()
|
||||
rows = []
|
||||
for m in self.market_list:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(m.get("name", ""))),
|
||||
ft.DataCell(ft.Text(m.get("url", ""))),
|
||||
ft.DataCell(ft.Text(m.get("memo", "")))
|
||||
])
|
||||
rows.append(row)
|
||||
self.market_table.rows = rows
|
||||
self.page.update()
|
||||
|
||||
def load_sold_products(self, e):
|
||||
self.logger.debug("MarketPage.load_sold_products() 호출됨")
|
||||
# DBManager를 이용해 각 마켓의 팔린상품 목록을 가져옴
|
||||
sold_products = self.db_manager.get_sold_products(self.market_list)
|
||||
self.logger.debug(f"Sold products fetched: {sold_products}")
|
||||
if self.on_products_fetched_callback:
|
||||
self.on_products_fetched_callback(sold_products)
|
||||
else:
|
||||
self.logger.debug("on_products_fetched_callback 미설정")
|
||||
self.page.update()
|
||||
|
||||
def add_market(self, e):
|
||||
self.logger.debug("MarketPage.add_market() 호출됨")
|
||||
# 마켓 추가 기능은 미구현
|
||||
snack = ft.SnackBar(ft.Text("마켓 추가 기능은 미구현입니다."))
|
||||
snack.open = True
|
||||
self.page.open(snack)
|
||||
self.page.update()
|
||||
|
||||
def get_content(self):
|
||||
return self.content
|
||||
|
|
@ -3,10 +3,7 @@ def filter_forbidden_words(products):
|
|||
logger = logging.getLogger("FletLogger")
|
||||
logger.debug("Entering filter_forbidden_words()")
|
||||
forbidden_words = ["bad", "illegal", "금지어"]
|
||||
filtered = []
|
||||
for product in products:
|
||||
if not any(word in product.get("name", "").lower() for word in forbidden_words):
|
||||
filtered.append(product)
|
||||
filtered = [p for p in products if not any(word in p.get("name", "").lower() for word in forbidden_words)]
|
||||
logger.debug(f"Filtered products (forbidden words): {filtered}")
|
||||
logger.debug("Exiting filter_forbidden_words()")
|
||||
return filtered
|
||||
|
|
@ -16,10 +13,7 @@ def filter_forbidden_categories(products):
|
|||
logger = logging.getLogger("FletLogger")
|
||||
logger.debug("Entering filter_forbidden_categories()")
|
||||
forbidden_categories = ["Forbidden Category"]
|
||||
filtered = []
|
||||
for product in products:
|
||||
if product.get("category", "") not in forbidden_categories:
|
||||
filtered.append(product)
|
||||
filtered = [p for p in products if p.get("category", "") not in forbidden_categories]
|
||||
logger.debug(f"Filtered products (forbidden categories): {filtered}")
|
||||
logger.debug("Exiting filter_forbidden_categories()")
|
||||
return filtered
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
# modules/product_page.py
|
||||
import flet as ft
|
||||
import logging
|
||||
from modules import product_filter, export, backend
|
||||
|
||||
class ProductPage:
|
||||
def __init__(self, page: ft.Page, logger: logging.Logger):
|
||||
self.page = page
|
||||
self.logger = logger
|
||||
self.products = [] # 전체 상품 목록
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
self.logger.debug("Building ProductPage content")
|
||||
# 버튼들
|
||||
self.forbidden_filter_btn = ft.ElevatedButton("금지어 필터링", on_click=self.filter_forbidden)
|
||||
self.category_filter_btn = ft.ElevatedButton("카테고리 필터링", on_click=self.filter_category)
|
||||
self.sourcing_market_dropdown = ft.Dropdown(
|
||||
label="소싱몰 목록",
|
||||
options=[ft.dropdown.Option("타오바오"), ft.dropdown.Option("1688")],
|
||||
key="sourcing_market"
|
||||
)
|
||||
self.source_btn = ft.ElevatedButton("소싱하기", on_click=self.sourcing_products)
|
||||
self.export_btn = ft.ElevatedButton("출력", on_click=self.export_products)
|
||||
# 상품 목록 테이블
|
||||
self.product_table = ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("상품명")),
|
||||
ft.DataColumn(ft.Text("카테고리")),
|
||||
ft.DataColumn(ft.Text("이미지 URL")),
|
||||
ft.DataColumn(ft.Text("소싱 URL")),
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="product_table"
|
||||
)
|
||||
self.content = ft.Column(
|
||||
[
|
||||
ft.Row([self.forbidden_filter_btn, self.category_filter_btn, self.sourcing_market_dropdown, self.source_btn, self.export_btn]),
|
||||
self.product_table
|
||||
],
|
||||
spacing=10,
|
||||
)
|
||||
|
||||
def set_products(self, products):
|
||||
self.logger.debug(f"ProductPage.set_products() 호출됨, 상품 수: {len(products)}")
|
||||
self.products = products
|
||||
self.update_table()
|
||||
|
||||
def update_table(self):
|
||||
rows = []
|
||||
for p in self.products:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(p.get("name", ""))),
|
||||
ft.DataCell(ft.Text(p.get("category", ""))),
|
||||
ft.DataCell(ft.Text(p.get("image_url", ""))),
|
||||
ft.DataCell(ft.Text(p.get("sourcing_url", ""))),
|
||||
])
|
||||
rows.append(row)
|
||||
self.product_table.rows = rows
|
||||
self.page.update()
|
||||
|
||||
def filter_forbidden(self, e):
|
||||
self.logger.debug("ProductPage.filter_forbidden() 호출됨")
|
||||
self.products = product_filter.filter_forbidden_words(self.products)
|
||||
self.update_table()
|
||||
|
||||
def filter_category(self, e):
|
||||
self.logger.debug("ProductPage.filter_category() 호출됨")
|
||||
self.products = product_filter.filter_forbidden_categories(self.products)
|
||||
self.update_table()
|
||||
|
||||
def sourcing_products(self, e):
|
||||
self.logger.debug("ProductPage.sourcing_products() 호출됨")
|
||||
selected_market = self.sourcing_market_dropdown.value
|
||||
sourced = []
|
||||
for product in self.products:
|
||||
sourcing_url = backend.sourcing_product(product.get("image_url", ""), selected_market)
|
||||
product["sourcing_url"] = sourcing_url
|
||||
sourced.append(product)
|
||||
self.products = sourced
|
||||
self.update_table()
|
||||
|
||||
def export_products(self, e):
|
||||
self.logger.debug("ProductPage.export_products() 호출됨")
|
||||
export.export_to_excel(self.products)
|
||||
|
||||
def get_content(self):
|
||||
return self.content
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import tomllib
|
||||
import os
|
||||
|
||||
def get_project_info():
|
||||
# pyproject.toml 파일의 경로: 프로젝트 루트에서 찾습니다.
|
||||
path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "pyproject.toml")
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
project = data.get("project", {})
|
||||
name = project.get("name", "App")
|
||||
window_title = project.get("window_title", "App")
|
||||
version = project.get("version", "0.0.0")
|
||||
authors = project.get("authors", [])
|
||||
if isinstance(authors, list):
|
||||
# authors가 딕셔너리의 리스트인 경우 name 필드 사용
|
||||
author_names = ", ".join(a.get("name", "") for a in authors)
|
||||
else:
|
||||
author_names = ""
|
||||
return {"name": name, "window_title": window_title, "version": version, "authors": author_names}
|
||||
except Exception as e:
|
||||
print("pyproject.toml 읽기 오류:", e)
|
||||
return {"name": "Unknown App", "window_title": "---", "version": "0.0.0", "authors": ""}
|
||||
|
|
@ -1,43 +1,52 @@
|
|||
import keyring
|
||||
import json
|
||||
import os
|
||||
|
||||
class SettingsManager:
|
||||
"""
|
||||
로컬 JSON 파일("settings.json")을 이용해 사용자 설정(로그인 정보, GUI 설정 등)을 저장/불러옵니다.
|
||||
"""
|
||||
def __init__(self, filename="settings.json"):
|
||||
self.filename = filename
|
||||
self.settings = {}
|
||||
self.load_settings()
|
||||
|
||||
def load_settings(self):
|
||||
try:
|
||||
if os.path.exists(self.filename):
|
||||
with open(self.filename, "r", encoding="utf-8") as f:
|
||||
self.settings = json.load(f)
|
||||
else:
|
||||
self.settings = {}
|
||||
except Exception as e:
|
||||
print("설정 불러오기 오류:", e)
|
||||
self.settings = {}
|
||||
|
||||
def save_settings(self):
|
||||
try:
|
||||
with open(self.filename, "w", encoding="utf-8") as f:
|
||||
json.dump(self.settings, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print("설정 저장 중 오류 발생:", e)
|
||||
def __init__(self, project_info):
|
||||
# project_info의 'name'을 보안 저장소의 SERVICE_NAME으로 사용
|
||||
self.service_name = project_info.get("name", "App")
|
||||
|
||||
def save_user_info(self, user_info: dict):
|
||||
self.settings["user"] = user_info
|
||||
self.save_settings()
|
||||
email = user_info.get("email", "")
|
||||
password = user_info.get("password", "")
|
||||
if email and password:
|
||||
keyring.set_password(self.service_name, email, password)
|
||||
# 민감하지 않은 정보만 JSON 파일에 저장
|
||||
non_sensitive = {k: v for k, v in user_info.items() if k != "password"}
|
||||
try:
|
||||
with open("user_settings.json", "w", encoding="utf-8") as f:
|
||||
json.dump(non_sensitive, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print("사용자 설정 저장 중 오류:", e)
|
||||
|
||||
def load_user_info(self) -> dict:
|
||||
return self.settings.get("user", {})
|
||||
user_info = {}
|
||||
if os.path.exists("user_settings.json"):
|
||||
try:
|
||||
with open("user_settings.json", "r", encoding="utf-8") as f:
|
||||
user_info = json.load(f)
|
||||
except Exception as e:
|
||||
print("사용자 설정 불러오기 오류:", e)
|
||||
email = user_info.get("email", "")
|
||||
if email:
|
||||
password = keyring.get_password(self.service_name, email)
|
||||
if password:
|
||||
user_info["password"] = password
|
||||
return user_info
|
||||
|
||||
def save_gui_settings(self, gui_settings: dict):
|
||||
self.settings["gui"] = gui_settings
|
||||
self.save_settings()
|
||||
try:
|
||||
with open("gui_settings.json", "w", encoding="utf-8") as f:
|
||||
json.dump(gui_settings, f, indent=4, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print("GUI 설정 저장 중 오류:", e)
|
||||
|
||||
def load_gui_settings(self) -> dict:
|
||||
return self.settings.get("gui", {})
|
||||
if os.path.exists("gui_settings.json"):
|
||||
try:
|
||||
with open("gui_settings.json", "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception as e:
|
||||
print("GUI 설정 불러오기 오류:", e)
|
||||
return {}
|
||||
|
|
|
|||
|
|
@ -209,6 +209,23 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi
|
|||
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
|
||||
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
|
||||
|
||||
[[package]]
|
||||
name = "backports-tarfile"
|
||||
version = "1.2.0"
|
||||
description = "Backport of CPython tarfile module"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.11\""
|
||||
files = [
|
||||
{file = "backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34"},
|
||||
{file = "backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["jaraco.test", "pytest (!=8.0.*)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
|
|
@ -221,6 +238,87 @@ files = [
|
|||
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
|
||||
{file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
|
||||
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
|
||||
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
|
||||
{file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
|
||||
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
|
||||
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.1"
|
||||
|
|
@ -336,6 +434,65 @@ files = [
|
|||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "44.0.2"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"linux\""
|
||||
files = [
|
||||
{file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"},
|
||||
{file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"},
|
||||
{file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"},
|
||||
{file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"},
|
||||
{file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"},
|
||||
{file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""]
|
||||
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||
nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""]
|
||||
pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
name = "deprecation"
|
||||
version = "2.1.0"
|
||||
|
|
@ -620,6 +777,31 @@ files = [
|
|||
[package.extras]
|
||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "importlib-metadata"
|
||||
version = "8.6.1"
|
||||
description = "Read metadata from Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.11\""
|
||||
files = [
|
||||
{file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"},
|
||||
{file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
zipp = ">=3.20"
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
perf = ["ipython"]
|
||||
test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
|
|
@ -632,6 +814,114 @@ files = [
|
|||
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-classes"
|
||||
version = "3.4.0"
|
||||
description = "Utility functions for Python class constructs"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"},
|
||||
{file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
more-itertools = "*"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-context"
|
||||
version = "6.0.1"
|
||||
description = "Useful decorators and context managers"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4"},
|
||||
{file = "jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
"backports.tarfile" = {version = "*", markers = "python_version < \"3.12\""}
|
||||
|
||||
[package.extras]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
test = ["portend", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
|
||||
|
||||
[[package]]
|
||||
name = "jaraco-functools"
|
||||
version = "4.1.0"
|
||||
description = "Functools like those found in stdlib"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649"},
|
||||
{file = "jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
more-itertools = "*"
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["jaraco.classes", "pytest (>=6,!=8.1.*)"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "jeepney"
|
||||
version = "0.9.0"
|
||||
description = "Low-level, pure Python DBus protocol wrapper."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"linux\""
|
||||
files = [
|
||||
{file = "jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683"},
|
||||
{file = "jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["async-timeout ; python_version < \"3.11\"", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
|
||||
trio = ["trio"]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "25.6.0"
|
||||
description = "Store and access your passwords safely."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd"},
|
||||
{file = "keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
importlib_metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""}
|
||||
"jaraco.classes" = "*"
|
||||
"jaraco.context" = "*"
|
||||
"jaraco.functools" = "*"
|
||||
jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
|
||||
pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""}
|
||||
SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
|
||||
completion = ["shtab (>=1.1.0)"]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["pyfakefs", "pytest (>=6,!=8.1.*)"]
|
||||
type = ["pygobject-stubs", "pytest-mypy", "shtab", "types-pywin32"]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.3.1"
|
||||
|
|
@ -788,6 +1078,18 @@ html5 = ["html5lib"]
|
|||
htmlsoup = ["BeautifulSoup4"]
|
||||
source = ["Cython (>=3.0.11,<3.1.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "10.6.0"
|
||||
description = "More routines for operating on iterables, beyond itertools"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"},
|
||||
{file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.2.0"
|
||||
|
|
@ -1252,6 +1554,19 @@ files = [
|
|||
dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"]
|
||||
test = ["pytest", "pytest-xdist", "setuptools"]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"linux\" and platform_python_implementation != \"PyPy\""
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.10.6"
|
||||
|
|
@ -1497,6 +1812,19 @@ files = [
|
|||
{file = "pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
description = "A (partial) reimplementation of pywin32 using ctypes/cffi"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"win32\""
|
||||
files = [
|
||||
{file = "pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755"},
|
||||
{file = "pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "realtime"
|
||||
version = "2.4.2"
|
||||
|
|
@ -1552,6 +1880,23 @@ urllib3 = ">=1.21.1,<3"
|
|||
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "secretstorage"
|
||||
version = "3.3.3"
|
||||
description = "Python bindings to FreeDesktop.org Secret Service API"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "sys_platform == \"linux\""
|
||||
files = [
|
||||
{file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"},
|
||||
{file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cryptography = ">=2.0"
|
||||
jeepney = ">=0.6"
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
|
|
@ -1925,7 +2270,28 @@ idna = ">=2.0"
|
|||
multidict = ">=4.0"
|
||||
propcache = ">=0.2.0"
|
||||
|
||||
[[package]]
|
||||
name = "zipp"
|
||||
version = "3.21.0"
|
||||
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "python_version == \"3.11\""
|
||||
files = [
|
||||
{file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
|
||||
{file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
|
||||
cover = ["pytest-cov"]
|
||||
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
|
||||
enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["big-O", "importlib-resources ; python_version < \"3.9\"", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11,<4.0"
|
||||
content-hash = "acbb4a6d39e2aff7e5bb4664bd9fc4bb0c8d00809d067b7016e98584f8ebec3d"
|
||||
content-hash = "a72b2533fbfa496663693cb7f0429d8c6f27e4f552e38580d14fb86eed879d33"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
[project]
|
||||
name = "Resell1"
|
||||
version = "1.0.0"
|
||||
description = "나도 좀 팔자"
|
||||
description = "나도 좀 같이 팔아보자는 팔판 소싱기"
|
||||
authors = [
|
||||
{name = "WhenRideMyCar",email = "kkebiini@gmail.com"}
|
||||
]
|
||||
|
|
@ -13,8 +13,10 @@ dependencies = [
|
|||
"openpyxl (>=3.1.5,<4.0.0)",
|
||||
"xlwings (>=0.33.11,<0.34.0)",
|
||||
"requests (>=2.32.3,<3.0.0)",
|
||||
"supabase (>=2.15.0,<3.0.0)"
|
||||
"supabase (>=2.15.0,<3.0.0)",
|
||||
"keyring (>=25.6.0,<26.0.0)"
|
||||
]
|
||||
window_title = "나도 좀 팔자 - [나팔]"
|
||||
|
||||
[tool.poetry]
|
||||
packages = [{include = "resell1", from = "src"}]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
from bs4 import BeautifulSoup
|
||||
from playwright.sync_api import sync_playwright
|
||||
from supabase import create_client, Client
|
||||
import getpass
|
||||
|
||||
# HTML 파일에서 스마트스토어 URL 추출 함수
|
||||
def extract_market_urls(html_file_path):
|
||||
with open(html_file_path, "r", encoding="utf-8") as f:
|
||||
html_content = f.read()
|
||||
soup = BeautifulSoup(html_content, "html.parser")
|
||||
links = soup.find_all("a", href=True)
|
||||
market_urls = []
|
||||
for link in links:
|
||||
href = link["href"]
|
||||
if href.startswith("https://smartstore.naver.com"):
|
||||
market_urls.append(href)
|
||||
# 중복 제거
|
||||
return list(set(market_urls))
|
||||
|
||||
# Playwright를 사용하여 마켓 정보 수집 함수
|
||||
def fetch_market_info(url):
|
||||
# 기본값 설정
|
||||
market_name = ""
|
||||
market_grade = ""
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
page = browser.new_page()
|
||||
try:
|
||||
page.goto(url, timeout=60000) # 60초 timeout
|
||||
page.wait_for_load_state("domcontentloaded", timeout=60000)
|
||||
|
||||
# market_name 추출
|
||||
try:
|
||||
name_elem = page.query_selector("div#pc-storeNameWidget span")
|
||||
if name_elem:
|
||||
market_name = name_elem.inner_text().strip()
|
||||
except Exception as e:
|
||||
print(f"Error fetching market_name from {url}: {e}")
|
||||
|
||||
# market_grade 추출
|
||||
try:
|
||||
grade_elem = page.query_selector("div#pc-sellerInfoWidget div > div > div > div:nth-child(1)")
|
||||
if grade_elem:
|
||||
spans = grade_elem.query_selector_all("span")
|
||||
if len(spans) >= 3:
|
||||
market_grade = spans[2].inner_text().strip()
|
||||
else:
|
||||
market_grade = grade_elem.inner_text().strip()
|
||||
except Exception as e:
|
||||
print(f"Error fetching market_grade from {url}: {e}")
|
||||
except Exception as e:
|
||||
print(f"Error loading page {url}: {e}")
|
||||
finally:
|
||||
browser.close()
|
||||
return market_name, market_grade
|
||||
|
||||
# Supabase 로그인 및 데이터 삽입 함수 (market_url 중복 검사 추가)
|
||||
def supabase_insert_markets(supabase_url: str, supabase_key: str, market_data: list):
|
||||
"""
|
||||
market_data: 리스트로 [(market_url, market_name, market_grade), ...]
|
||||
"""
|
||||
supabase: Client = create_client(supabase_url, supabase_key)
|
||||
|
||||
for url, name, grade in market_data:
|
||||
# 중복 검사: market_url이 이미 존재하는지 확인
|
||||
existing = supabase.table("markets").select("*").eq("market_url", url).execute()
|
||||
if existing.data:
|
||||
print(f"{url} 은(는) 이미 존재합니다. 건너뜁니다.")
|
||||
continue
|
||||
|
||||
data = {
|
||||
"market_name": name,
|
||||
"market_url": url,
|
||||
"market_grade": grade,
|
||||
"market_memo": ""
|
||||
}
|
||||
try:
|
||||
response = supabase.table("markets").insert(data).execute()
|
||||
if response.get("error"):
|
||||
print(f"Failed to insert {url}: {response['error']['message']}")
|
||||
else:
|
||||
print(f"Inserted {url} successfully.")
|
||||
except Exception as e:
|
||||
print(f"Exception inserting {url}: {e}")
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python module.py <html_file_path>")
|
||||
sys.exit(1)
|
||||
|
||||
html_file_path = sys.argv[1]
|
||||
|
||||
# 1. HTML 파일에서 마켓 URL 추출
|
||||
market_urls = extract_market_urls(html_file_path)
|
||||
print(f"총 {len(market_urls)}개의 스마트스토어 URL을 찾았습니다.")
|
||||
|
||||
# 2. 각 URL에 대해 Playwright로 정보 수집
|
||||
market_data = []
|
||||
for url in market_urls:
|
||||
print(f"Processing {url} ...")
|
||||
name, grade = fetch_market_info(url)
|
||||
print(f" market_name: {name}")
|
||||
print(f" market_grade: {grade}")
|
||||
market_data.append((url, name, grade))
|
||||
|
||||
# 3. Supabase 자격 증명 입력받기
|
||||
print("Supabase 로그인 정보를 입력하세요.")
|
||||
supabase_url = input("Supabase URL: ").strip()
|
||||
supabase_id = input("Supabase Email (ID): ").strip()
|
||||
supabase_pw = getpass.getpass("Supabase Password: ").strip()
|
||||
|
||||
# Supabase 클라이언트 생성 및 로그인 (실제 환경에 따라 인증 방식이 다를 수 있음)
|
||||
supabase: Client = create_client(supabase_url, supabase_pw)
|
||||
try:
|
||||
auth_response = supabase.auth.sign_in(email=supabase_id, password=supabase_pw)
|
||||
if auth_response.get("error"):
|
||||
print(f"Supabase 로그인 실패: {auth_response['error']['message']}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("Supabase 로그인 성공!")
|
||||
except Exception as e:
|
||||
print(f"Supabase 로그인 예외: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 4. 수집한 데이터 Supabase에 삽입 (중복 검사 포함)
|
||||
supabase_insert_markets(supabase_url, supabase_pw, market_data)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"username": "leensoo1nt@gmail.com",
|
||||
"id": "909d2ef8-7053-4006-ab40-49eb49f20383"
|
||||
}
|
||||
Loading…
Reference in New Issue