From ee77982bc70231dc30cbf27c8fba9b6bd1b86b0e Mon Sep 17 00:00:00 2001 From: Envy_PC Date: Sun, 30 Mar 2025 08:29:07 +0900 Subject: [PATCH] ... --- app.log | 200 ++++++++++ main.py | 197 +--------- .../__pycache__/app_manager.cpython-311.pyc | Bin 0 -> 6982 bytes modules/__pycache__/app_state.cpython-311.pyc | Bin 0 -> 1358 bytes modules/__pycache__/backend.cpython-311.pyc | Bin 2753 -> 2753 bytes .../__pycache__/category_page.cpython-311.pyc | Bin 0 -> 1753 bytes .../__pycache__/db_manager.cpython-311.pyc | Bin 10000 -> 7540 bytes modules/__pycache__/export.cpython-311.pyc | Bin 2163 -> 2163 bytes .../forbidden_page.cpython-311.pyc | Bin 0 -> 1753 bytes modules/__pycache__/logger.cpython-311.pyc | Bin 3391 -> 3546 bytes .../__pycache__/login_page.cpython-311.pyc | Bin 0 -> 7867 bytes .../__pycache__/market_page.cpython-311.pyc | Bin 0 -> 5371 bytes .../product_filter.cpython-311.pyc | Bin 2171 -> 2407 bytes .../__pycache__/product_page.cpython-311.pyc | Bin 0 -> 6901 bytes .../__pycache__/project_info.cpython-311.pyc | Bin 0 -> 2128 bytes .../setting_manager.cpython-311.pyc | Bin 3684 -> 4900 bytes modules/app_manager.py | 106 +++++ modules/app_state.py | 17 + modules/category_page.py | 18 + modules/db_manager.py | 50 +-- modules/forbidden_page.py | 18 + modules/logger.py | 39 +- modules/login.py | 86 ---- modules/login_page.py | 126 ++++++ modules/main_window.py | 203 ---------- modules/market_page.py | 75 ++++ modules/product_filter.py | 10 +- modules/product_page.py | 89 +++++ modules/project_info.py | 23 ++ modules/setting_manager.py | 71 ++-- poetry.lock | 368 +++++++++++++++++- pyproject.toml | 6 +- tests/input_market.py | 133 +++++++ user_settings.json | 4 + 34 files changed, 1259 insertions(+), 580 deletions(-) create mode 100644 modules/__pycache__/app_manager.cpython-311.pyc create mode 100644 modules/__pycache__/app_state.cpython-311.pyc create mode 100644 modules/__pycache__/category_page.cpython-311.pyc create mode 100644 modules/__pycache__/forbidden_page.cpython-311.pyc create mode 100644 modules/__pycache__/login_page.cpython-311.pyc create mode 100644 modules/__pycache__/market_page.cpython-311.pyc create mode 100644 modules/__pycache__/product_page.cpython-311.pyc create mode 100644 modules/__pycache__/project_info.cpython-311.pyc create mode 100644 modules/app_manager.py create mode 100644 modules/app_state.py create mode 100644 modules/category_page.py create mode 100644 modules/forbidden_page.py delete mode 100644 modules/login.py create mode 100644 modules/login_page.py delete mode 100644 modules/main_window.py create mode 100644 modules/market_page.py create mode 100644 modules/product_page.py create mode 100644 modules/project_info.py create mode 100644 tests/input_market.py create mode 100644 user_settings.json diff --git a/app.log b/app.log index 1654dc0..198782d 100644 --- a/app.log +++ b/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 diff --git a/main.py b/main.py index 445b7c9..cbc6772 100644 --- a/main.py +++ b/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) +ft.app(target=main) diff --git a/modules/__pycache__/app_manager.cpython-311.pyc b/modules/__pycache__/app_manager.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..08d8f5e33ac1c93dc2b582896ea2f6befa5aac9a GIT binary patch literal 6982 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 Y-v1M7WOX`_=j!xhQvb;stwaw01;wpCy8r+H literal 0 HcmV?d00001 diff --git a/modules/__pycache__/app_state.cpython-311.pyc b/modules/__pycache__/app_state.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..587f0879507f35d80b8ed72d556066aa4d41c30d GIT binary patch literal 1358 zcmb7D&u$%HTi!BE)FEQdCY>45`> zCLZzFlS=$&dg!6qtBGd}dg;mcX1goUdgv_s&HJ(Q=6l}^Uvs%7!14Oo&(v~)r7F5-Lynv zU{{mEHg*#2lt{@m#?xp!7$@67J0mh!e<^U@dS1n^`?7&d<3T$m@SScTjGzsNz~iJ~ z=*3?Pz&chrNOpRycDG~&nZA~ax4X?=kZtspl3kyB^_Cp7z}B+U)U?#S*w}el^Y&}+ zCB{0>Yr8$s??_#%dmh)sXWQEk9FBF`KIaE35zXx=MD+B1gd;dFJQ~`^x6ju$hxVCM zI5od1@eOR^(TIejP~6%bbVhJU%~0G7YG%yJW2RE*mn@aRN#zhUt#e&PCmZCn^m*Lo z!M&K=Ts%$7Xe}{ZCx%0ORF*2Fl=SlQR8nxKWJrV2Sha z?xLOLe6L^ckd~Y)S{vA+-SE{4k)aD0B($&819BfCjKNei=cwrH4UmBP6T(saOe`z; zWCAfbEcy5Z12`>03S*!B#i;obQ(NCQDYdl-m@1x_RFP8GLdcb9mP;(8>!S0og$w6Z hsKpyq!~|Vv{Q_ZodQg;mIWI340}#j=Jln{odQg;mIWI340}!0%e!7u+8y5gNp#{1C diff --git a/modules/__pycache__/category_page.cpython-311.pyc b/modules/__pycache__/category_page.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..36803780bf96ea8f0775905745d5f95303cb3a31 GIT binary patch literal 1753 zcmZuxO=ufO6rNe_N~^!NA&UG{B~_q=s41o`^`RwA52@`-5XC8ks)9w*jMl5JC1nMTkSx5*1nnOfbLBv&ulRNPB zM#4q#brcg1_%fd9@7j~2@d_+!h}zl`0>RXk8}L9Um2&REv8+g3D`LV!#1)i~i3I2+ zZqV;xfvYK#ss*F`X-cI`T;k}~3#v+ujB9WnW=`|(_-lX~s^AJzfa;Bk04waQP|l&Z zoEr_ed?vUK5<89*MisO*Y|Rm_1GVF8znM4oKQk(L~>2JbPbKp7`Yu<*cVr{NHY8Zw7?FM>|+$i9;TA`n+Y z94#~=HQaU3xZ{K$Qt8kmdK_EC1E=IsJ#fk?zL8Tr8}_K6oTC?LM_Lt#x8XaX%_&|x z<^Fi-ENJQczI*h|`r&U|?Rq16xLf7Z(YLodcXm7Xst4mB!NhbyCoutX4CTPwUr|k6 zFqxFrQ<*PeTPm7moiZs?&RBMr+P!tr^X_dCLlyo8fXRT;^4{9|~{GS&@X5)~ryj8OpUnIbNjuV-p+Y ztvBypx^Fhh*7*h#%6&XScS(3g0Yc`lI6W-F6`!^YJdsrxvNAFrT9IdKKt^ zqTDQ{^d86Pm4y9o^YFhKB!{`L*}pe~2hWbMaUQ5`WGn8)ofoIUfmd(w3xKNgbOlj5 zdwYKKMi&8bUv1=V;$;Z9q&eJ0L+Ha&q>D~M&n~wX6PcLRscE}yT!04PQs_4S!@~N)A&Y~Dt%c*xSC~Rz*D~XH27`)7#kx72!b*yi z+wL^$W;HSz$2ymukRvMA8T`4<a~i%=aC<+7jZyf#3hg=SEcL&CgXB|J zfH=9`WX0u$Ecw&rBY(J@R)p;h>HD+R*fda)c6{OW`<< zs>)G=nCP2D6p4mP8-<`raU1bl2K15^H_F(yEj-om(tUPOb!vmc4A>HY#Xe`7cU*L6 zjiJZ>M|-o5qR?Lw`Zb|{i{UNqb+mKw%QX~OMS)vQMbuG39cw7Gib5+LMbueBoolFP z74;NRZwd8gkA>F#`)^EZ?!ybG7ZVGK6;HNjU3AVJpYOZar!^m1@>8Jg@!OBy&Gr?= zfs#0&i396(uV%yKBx>DxVChY*`2cPAWA_r;N5h6_q$G}L;t08nntjIo+&Xfw%L5*G ztlb{=!JgNtsGEru%qA(803HhRs2e}(nipB+gerB+L2YY>g;OVU9GChfBMHes6>iXsXyADy9v!cF(~P_{n_O9Ccs9y{-^5ummp-YTB_tWINA(#UuV3NWo&=E|25M*jw8=1BF zu>7k9c@eZG=hv>ivo?D-d3iE<`NQ&*+m$xlO5nzoVdcLGoLb>DW;MQR#b?e|C}tGG zFSKPsD7#YlpVlBWkfgxnScGA+09Of@NV~9_lZ$JFt9YL9_Z-tPzhXi$s7?KO0REIW ze^bV=?IX8v4Eq?qbYazJl}O@ig-^7lg_`Y}th#VR3&Vu}l2+lb+1Z89QfL=KTKiG0 zH{~sDHmKh;4-&T&Fe`lNf@1?%J64K$sm&e?vt#^A!hx*aqq+&=eQZ&Y;mNqE$LfQI zTLk!mFI|wY9>pn)#r(wEMEgd44nK#TCr)AKaEGzwg1vsk%kcvveM}$cb-8`sewWX~ z@vec9-cg|%qpM*z8osF{YSIn2Dy)1cun@hIVV69O{= z3xZsLgsPVx9d;$MJlqi1<4q{Iw-RdZJlEs%atS5t_PP_q5I4eCAOi+L34lY%7a+L` z!HWnE0eH}X3I8b61#&XigqC!{_B5%=zdVY5gr^2V=^_@_7_R8wGf$aCsxf>HApJgS z>yX+`Pnshdm!xrt8rQl)r!9z+)|ns0$g(I|c883R6%ttyBVP_g$(O~d<_Os$ku5Q@ zElReDuYF(aa7D;75_u*@_D9M72stQ`gOjhs&EysTI;Juf-OOF;_^>Nda!@Kc7;;BT z4v8g);+DJ_{haw{W|47*JMMKotdOdkL5BCy$S;j@nc$_Sp)(Xo-w6!{1BHF6qeQ@%Am)PZ=bVdx_lA&8P zbPI_hTbUjHOz?PRof`XN@gBhYYF)jWxSzeZUP0VHf`j}(p#l`fvJ3DtLQw1YrfF`; zOmj~eN?TklysIM*er1wnYq5Z@Jv7`s~$j{ZXy3F+SD}}>kDXtezle^PglgAn+ zl$lCPPpPNIubNP8qaNe-7Q1cE5qtpiXANKz#7&$vPGk+>H*voj?SL>%s5dEwnoxfg zH~2M3F<_q1d<{ETg4xex2hH1>6gxn@jU94!vO}+LXlRQa%F;Z+R`cJ1Gvy5dgedS6 z`12{g@RdVj=gsjXbP+C(Rry8`-7>%d^6bj|ec4Kg2~q;ABbJU5qda2R8vk^hrx2C={@I^Q?%8E3$1|g z{^h%WZbqcnDH%FNLnrJJ7WYl4(XktjA$T0G-;e#F{CEvi5BBRC_7e{d?QJL{KHG(Z z{8^a_aAhp}PYUx!+JR9j>x9O zXAK5b!JX1|462Y_sIcXi+AQfFs6zI2462=A7Yw#yJ*H8;im#_oaN4@}xzMan7x5p> zk>OF+H^kNOJum=)ig|txuukAO{#bZy%M(7g)iu6}D27GS%&z6GSMF-% zRz?&Zd@$Jb2iz(L2V6*%I2I)bA9CoShxWb-f|vzEp-`|TgNl2~sc%-^jW+J=?lKG!0}i3T7~>B9ggAZ|hz25vU=3xlGjmyvLo4nqQA+gxUO%(ri;s29y*PAe``lI9Ho3=DR9 zTOc%FQSErXQq+iCDCs8E&2+%#%T!aItt+$Gk|aHS^OMW7i`Cg{6!hAAvkN6s(P(*A zag!vo{7fuXZBdq4Oj%w|`gStm%31jGM*wxSml$2;*TZ|s6im@QF}5CkB91*4$E@)W z+TvtKoV3JA5I+&eAB*E{G1U=Mmir!q3smqj1xyG&rxJ%iG>|!@dzN>CeR8h~gd>3# z?{79Dw1Q3WxZ_m-s_D@_KVB>n0xQw)2dumX^jWcbIHQKrTkv9L*7G*nCk} zN_Oz$l2%#J?a)b6ZexS_DYFbqMikHp11z8Qx0kPxW6WR=j5|ix`%@53`Dp5=i z3KH?BtfF8+5Ai1$Mex#t;MHR)0n1+WBxqBJ=t(^IX4AyhoOLqu?R#(LP2M+e=Dlsw zn;lQ>b}IwzqwBMB(LCX}i;hlY2c7r|bKDZP09wOVKwB6cVkFzd+(vdg`?>@DLi;Z< zj|@-hy|9IaB>P7{W2?$}^AoT?ugxJQjA2j9SX%R#4th%W@?WQ3m99+AX70FWuRkkY zn{>}zy-~_!=Q307+1#Dd^~uuHaqWRA;!ZUlNGOU-+>v-B83{%sDLI@&IvHhDO>zFx*mB z%6z#@d_b-Vtf~Sq&dk`BX?=KWx95|%2asd^M1)NwQ-xv!$Ydozjx~6aE|68a$vh>U z-xZcmk0~j!l24zZb-;q}ZUCTF$;)l$V{_I{)8Z>)U!mIn6%Qi8pD!e#bNl>zhRIqm zFAATdSxVU;_|1?!AJq(s^}&lY_?{x}DOCIa3*LNeN5H?&zqdYq^Z6i&N8&q_lw&>t z6@x+#K&@xdKw0z^abKa@{~rr}cOWnvsOP}Tgd_;upk|{a_zblh;wJ{ss}2PL+d9*= z>qsaw;-g?Z#dTEbrLcj*CV(7cFsnQ~o?0o;q22@-ul;7$cwcRK*IhUz7f+onGmZSf z-{56dOWBqw{zRD(czA$|p%L-DWu^uK;(b(|2T<2jbsj)nxVYv<+O=uh{vM;GAR|)J zPeY{zs!}|3t0E^ypk}~5B?DmY)rQ0tn{hhGFA1bUONh-I*1$}~pROTWD9|;=IsC)} ou|#+*DsLm(z(zM$Rqc(q-s?pWRy};9wjX~c4r{IURUF;#H^T8>ivR!s delta 1514 zcmZuw*>4*~7@zg-?5>@44$=e?oM=f~kOjFAr8J}op=pg#4&gRx1p>>>Zrp&6Xx4_- zaj~lw$tZnjSfVVDR7MJ<7JUdL4|!;5{{*Ls)MzChctC>eS~4oURERR;E2QaY<~K9n zHOKe+^k?rhS1;S`Hh`PB{hR+ieztm!8k}7kgw#pkU^Jw*LO@e+$ATy$EU zrr+6bEH7=ZEEuavGo|&26JZqeMIy4~4a$n*^UFO-1gQ_`NqWR-Q{zm~=HzuokpmM* zP#-bPoDKu>$8tbzh373fpla|BHp_S7uBKTBrK`8uPhoo;q@g;%)i;4S6}NnE0pJHb zitQqUwG7d!cP;e^$#N2q;iRLVVFR03YDVTub7sL@P8#3c61Ue^jBginbK3T5`X$jd z<`&J{3&zTi=Hi-YUQZb_OGai*G}0;KryIuV716x>xq10gL7Tp0ew{I{Er@TO8Xgjj zo9pKFYv%l#xrnXOSB%8W&D>nZm?1GbDu+L$Y$Hxqw?>d}Tt4d?|46qLKguhgA4fI# zo;n=}`;kCaPnA^u=w$g5oe(t;6<7LH^bFbY*x5T7nh5K4m2ZEo?4m$yQg}OZ;1$}B zA!Zv2`~5QNz-Kk#y^_R`1bH3^`_$jLqlxY<@xXoY@ICSHI(N5WLv(G5uB_0qCDh&* zTJ8xg+1Abtp=(p<%9eW`TCCl9fC07QVB<7~awA+K`vE4}&B&&U^zTE>2JR9&OaPHYK@`e1eP9s^;*{LX+ z3dphgUS-M`5|zk>fPA!VJRAt4o*4alTVIUp?Ifx`Cx`6#N;(`o{^poVXWSzvMs@D= zfM>)#;?m)9k7v}Q^T#~yQ|@B}Lptr3BPHITgV?XgnS`|v+8gvu>9DYToG1sdZLFz8 zl)Y3WYUO)?WQZsJ}X-y+Ec>?F^ZQwL^H?6dc;Qd==YZOIh>Ppssn| z-tmQ9lhda+YTGw!+f#6>rawKFt?9@8#wW#@Am3kLlhdk3stz6@uSOjNS_pI!Aaq9L zE*VfTEs~rh9T6ZynNWYNa*Wo&t~}Tc&x1DSl4Bj_ffZu%?uJGIzCq=|lM%J55#eo0 zQ11whjij!08~$c5t|weps22-fRt>>%hSY+S!Zlw^SuHV`4Ea0G`9e}aMhYTyM$Zu_ z+?jZyBLs+N;V}>S-e6dY2IM}}hh?N-lxfv#@9ut)A{Trz{#lU!^?&WVl=`#%X*FwS H84~P&&@@U)P(s_(m)0?UV=x(P_RTmD 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 literal 0 HcmV?d00001 diff --git a/modules/__pycache__/market_page.cpython-311.pyc b/modules/__pycache__/market_page.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d50a202883ff194e65e4298fe0ddb567abf33438 GIT binary patch literal 5371 zcmb_gU2GHC6~1GSov}SmoIt?DKQY~q;+6$gn}jT)AVPrchK;rf6;eu>bv%O!_$M9P z>=Ff)wyYGfk#@Dsibzc=x1u3Hq~@Vr@zAF}@z95vXrx#pAt6;%T5KdE=nGFh=h_~7 z@&f|(dVKD^=iYnn-#OnoGq>GtCxLW#-+yBNbP@7ztW*Sg49zVg z7RW7eYl0nN8Db$PiDiQ{E|CMuiw>CI zsXIT8$d{zdCor+jjw7|lQ!!kHAsyu6x~=eko&j=`Ofr*1fKoY4TA;=lH7nGNeZ~mX za3oD4)(p9mBxkF()aMIOxo0pKINxD%hZzjpW!$3NfCNSAt^{4X5Fbltbbl%-jLRu8 z8_lGJ^HL^yK@x>%Bpx4)L@(*?(d<|p>ZxQ#N@l{W&ZedK`85L5xU>+YmmU2F&yHU? zJ0wA`_34>wv5&?+Z+^8t2XL*^hZR&+)5V+X~mFa(<2TE7j*Yh#*npe4*liKfR3yaL#ZW z+Gm6DxO19J?xzM#QQ#y|GrNZI1p(CUP2gp*yj7jP65c zPfMR?TGy+OUMm3Z1_$IuyYujrAT-3Sr5$FzB$nn#z zRc^-$w?k<=pmMz$*Q;>7kK1=@?faGYPAO-8p`D2lv8QFmJwG}Z z%O%xdw-)SHJw2MI=b^!ltHJlR;QOklOY?Nq*ca5`J`k*W_G_N~r3SXOvE(EzTk~7C z=RLtvvm@YqMu5CPg0t=T?^fb%o4JDX*=JeJyJu0<_Wtximo{)t*)FJ_Pc_e{dCykO zv$JHgx*JOzY299O*}WhK$O|Ok7x+>maky8xzzP?b6;WHhd*;a#DE_dtEY zZ{OM%fK3|`bA~SfxkfU3vASS8v2PUq8HYTot{gc+5fkDPH zpdebrHD1Y#CFYt*_S?<~(UckE%yXD)|CbqUZ!%-dy0pfVU1cXNIm0K`pWY;EUM)kW z^BBEdH110<{9`@R=L}C$FU_nK6%=n6%>EEM)d4|0c44HpI~;mCd%yVIT;c0QolQm( zl1_AMR*vgzLQ13nD{WDTy3*mMa;Vn^;wtf83;?l=i)k4*DEolu4TxL_PaK6X7%T5W zz7@Lb_Q?3SloTnL$*njBfn2v|$3=kLu#Ezl&PcRPXA-&g!qY^{BpH&DV>N;mtQSPqF#tR;_twiP#+hN~VrJ zY}q>VBSa$CeBa#3+|k>||9pJ0bNL5qN4M6|t@?X3e~;=ppm`3g?X+vTMeXR)I=WPU zx8|>dy4OkqgzIQ55q3+{GbAsNOdSKrb2ooAaARQBs`7q~_bc?t@7zTpPve4!=Z;m* zyTW-@u1(|Gl%^l>%j_o7-5H5-6>ST63S_lP}wqR`2(4^m`5@g zWE;Vfylt~rm&tYGl5H@uIwjd;&NV?6oekBLrknv>RSxDG8Xr5DMNM3*sHQlf-ux!E z4?_qMs)i7u%4^fXP_`4qyy}H2RQT)t;`Lj_+_zzFt(i8XnVRM-8!LiL`Hia^N?|KC zJ-G+Uy4%z}jXGi&UprJL0iCe!^PW}y-4*`b#e~Wq()dFPeID_i z>48-~u)+tF;0cvKsqrTj{^XmO1u7Obi6)hZXKH28a}nm5!gO{vpjjqy~ z7tpwX!UbNZPXMS!!ZOt-j26xMG)!cStHO-D8#0;tE$m))bh-;ow-V$}Y^&Q}9seWf zo<{Er8*JJ#J*jX$iu8kFH+~U8hb{=ZQxM8uO`yyR!mqNCc)6wQvswDVsR2JV!Ap^D zKSjSe$@rc?UnOL8xiXr*jAtz$1#*YbdCKn_sEINXRL=AlK&~0TlErQd%nr?+0e5N( zEXJ0Sw6KA@(RI)M*^N2Jn)hACHuhCzg1QqjBVvjQn QtZifZjPhS>(n?qIKfBtAQvd(} literal 0 HcmV?d00001 diff --git a/modules/__pycache__/product_filter.cpython-311.pyc b/modules/__pycache__/product_filter.cpython-311.pyc index 043ab33b035afd8d8512b89976149c27bf0da80a..a8f8b854af3667cb264c4f75ab342a168b58b123 100644 GIT binary patch literal 2407 zcmc&#-%A`v9G}^p)6;NVHCG#=rkm1KFG){Rm5X8};6+X<7fEU81%sTo>-J#pM`rJm zc${z)K@EAZ52=AtkUVIDgoaY+J@!x74GTH;skBhik_W*T@u}b0_4d3wmAsVB&3)&) zGvAr{&gVOy?`(NItq4kM|6lS89-%+kN2TBcWw#xabtIxCbQ_5nBo?_Zk)MxOESMpI zKdgXu{{XQ}z#fZ8L5sM6HxApus9i*E6Z*HEu|=-HZE%&*!>!2IT4E$4kvzo@$qJdr zqHD=rKq0gVQ?{LHi~O;xH5K?D(2sSyV|1*2bN%iG9!~F${LMBqTPGALJC&AYiRRB= zPh};VNXf!{LIG`dK27Hmq9~_yI1jd+#>;8#|`@o zuzeJ3pDz%GV8cEE7P?h#19}^a2;tp?U~M}AJ37d>SWqdxlrnIsiUN+yc< zIg^*Atm#tH_azD-ae;o5Fw9JxQQ5T;gyMcrXk;d{FmqE_MN(%n6qI;& z*2X-LS=etGm8F#QAVbIUgGWaqFsh{EF-08%C81{H>0}1f$}9%GIu2s*4SIu$NDCxc z{VMt_`||Fuxu0{TTfg1ZgJB~WejSXw3P$wcv=N-v`lb&oZF@`ZGVuJ^90%k$4+fxSI&+nciMTI|I@jc0ch_|yYQ1LUQo zaVZIQYuK@;jPdyk6V@e`MBxBoRp@nm7OV^H9})7x^#_UUNq7D^Z#FAH5U?>@Aj&pb zOqVFl}zLk1br$WXasNV973M!A9>!1ClO z2;G}9ys2W?nKoWJqqU7g)k(-8A&rEbp#hx?8e~u-gXL4Fi#IGlYgp%`%iqDz|2kcY zME<{%C1oV0?9};Bj?a?uSXPqLG$BE%%(qWevZpX^_b=2y=p@Jxtopc;9=og;2YMPy zW71mX?%|)#7Fc5JhQ!F3?gI}ND0>(En4V(IVcPl}3d~l$t)wXHZqoN*PQI_zCs z%07(s>SrJ<;zl(ut81~1yIZ-Zxo5Xa?Rw9s(KD)rzSeqJ1-e=-NSm$~Po!&gbH>>V z$8xr6*XySp?C>sn78;A2w3BIFOq>T%&&kdcj!Zv-2@Fhi8N|C1b66)M1{u-FNV%(D z>%5|oGi3`H8y-f>sP2s#-smA(hD+aQZDFW7nKa0xMkXCvUe(F4L54LlT%{#EK33MS sJbaEUFzp=d9<`sCG5Rt1Y-N>CLD}Ik#ygxF`*x5ce2!M_W16+xzxM880ssI2 delta 930 zcmZuvUr19?96o3Fo_p`w+;UD$Z<)e0u3*MmBrY{ZV1(pD#6T!f@AWpYZJbR@xuOq2 zCXtTwBK6Q-(jw@&haT!ZVT(a>E9fPXo(8^1z4SYGF<18EoR8o6o%5YP-*>(TejW8~ z)zuwA5T88X%w93)8}Jht3DEZ<7@_+lH>QUc1rsfb7P^FPgjY#c=7dVDkZG)wdx|Bn zZQLeh8Z9v|vUemHd_XpV{}Os7FnV}4_ozjXEHSq~ghkeeZ!yL7M3K`prO!@f*)!Jw z*4UQNaoseM#@#d>RDha^0qpOhU9^k}fjG3++4acA`KQsx(fr(VT2#X&HT+TS`=Is} z)&7#&UufvBEUd*n#9G9WC5_nfOv*G9Nz-`=?WMGNJ26a{Nw;>KR*^PRwKtYUtF*FDwDH^SDU;oYz#D9tT=; zEsv@&%k{~;VV}j=mHOq&I}K_Xxk#r2EAkhAp!N-Kmta&3#7ltwc*!3xBkbUD|Yk zeb6gm33HepxN9AD_Y8Me=I-`1EB|_LiPwLH%LemB)(;x@_HE9!oCedb|b0g?6)+@Ihr>#8yTk-aa;wr<6>Z2!NfiwRK682R* z#qvty&|#T(Jb|_7YIJ=eA1->jN}jI5=~%(T+yAtEuS86)?N!a?IOrVn0#RObj{m7# lxD7vg#u%g>P;(safut;8jLR+$4wg|BTC3SXjc3kwzX64n@b3Ts diff --git a/modules/__pycache__/product_page.cpython-311.pyc b/modules/__pycache__/product_page.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28e44c2887cea03c1511668b0149ad0fe7b2ec71 GIT binary patch literal 6901 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 K?`mY|1N%32GkyR7 literal 3684 zcmcIn|4$r66rb5IcYDWOkF(Mu{kAPwIianWT5YLXX{rs?Vp5_>Nt4ZmUGNATIJ+mc zp$)10K$Jv`HAPbt6N(V{0b|uBYEt?a+%+U6oA8tIhxV@VD#nohdX6DVC&%EW^V9-yXd=mJ*ZA2#IH>|it%7ew{09++H(P;;1cHXp^3gjfwg?^%o z;{qX5@N-JDpwlK2k?vu&u_fQiU#`yLJ5aq!4AM+>g8I#(zDXDRX|tqD{iIpeWsn}- zV|ZHy-P5bvn#PDk{#(+vm-aMUrs*w{DzvnJPj!O)sUL) zAIpw@UJWDM|LJCS%=b5H)st^GL+DkM*PKgt!QMcAhMps(8&14+VRG~&8Sg<9u zF(TSZYrMlq#5xUIaaUjmJGx@J=2jzuEty6~EAzqtCJwKvkE&19o$LPK+!+IUI-=(~ zyYys-VV=v|Xr0a$@9+M=Rx~Z1h+CR=p}eU$0Q=n@1qZGM(MRUX)d6WRIA31DUrOMv zQgKJANGsdtl+7RSMEs$hGFT%=aXvVJ?RikDtmJTAlxcas+&5In_XaUxVnqQsPX zg!AHA(nBdZODH@TJ0A5FBQ%jpVwJih5>iFjo{NIDdAUWGdWF?<6pwNiM!qOq5^#J6 z_0S&T%pH*%ZOR_WNvn0=;Yn&MMxv!l2QnqM!4wV&RQF(y-M=}bW{1Zz*N5vOep~1= zZK*qEy=!}n^KsKM`9ZR!u5Ke?OGqzU!UMbR}M*6fWKq0;dJ2OT;O0TaBzP6 z&Vdtyrw3L%Fv(I*1~Yh@uY&8BkJi_bUn}aX#M!MS0B5VD`fBfNwTQTeBCh2)>cy5i z|M9)zZE5TA-Qw-tGTEqzb)xc4YbOuV%+S&2vM|%v{TUI8qn!vMr#-S;4jfvL!v)+0E&| zp2D6A6iBq9<8KGe*!xD|nqEtUqV94u{{sXPLGm402$v1%S2{jZAh0k%#}6(fYT2uj-n%h0MG&qSk=B#XDxP^@UJFI zzas^x@5r*?6nK6nmJS@A3mi_Z_s1C^f)z602>5(?Ykin}v9n$he<}eqD@pZ=cUBP* zmr%q3j>G=?7sT0ZrS(y9HYx+QeM!?`kic5Iic&H%fV1E@04F8419FN^UjhKBxWEOs z2jAC%8vOBaw|i2*dAtBNtP(0J|1=M^kD$tG+B0eeOGlFUzOLI2zv&M^KRN=n_*2- z?xu;G<=pG`CVquK#=+wLO0YDMBj8|Y^Weq1A@xp39o;hVgmb~Clv>BBKwC0i;LHCV zPZ1_#5m=QQ$0#n-`FwKX80K+Wxuy%d*A3-L6%3>s&0RG-ng)CwJvN&ELlh6fGT1&8 z2XH|vf*0n&S-Ir3Uae7i76(}y!mT^5WtcxA0sOAS&sI&d{hH={ ze*z9@+Iz`ZN503aX?j-W(@kh!_&VIhaUV%1@t=Rwq h?tV(=Nl71n=83P5KRM|c3e&C?53%j9bvpP|{{R54ZH)i` diff --git a/modules/app_manager.py b/modules/app_manager.py new file mode 100644 index 0000000..ee58a95 --- /dev/null +++ b/modules/app_manager.py @@ -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() diff --git a/modules/app_state.py b/modules/app_state.py new file mode 100644 index 0000000..f9524b3 --- /dev/null +++ b/modules/app_state.py @@ -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 diff --git a/modules/category_page.py b/modules/category_page.py new file mode 100644 index 0000000..719147a --- /dev/null +++ b/modules/category_page.py @@ -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 diff --git a/modules/db_manager.py b/modules/db_manager.py index 45b9622..59f76ec 100644 --- a/modules/db_manager.py +++ b/modules/db_manager.py @@ -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 [] diff --git a/modules/forbidden_page.py b/modules/forbidden_page.py new file mode 100644 index 0000000..f59809c --- /dev/null +++ b/modules/forbidden_page.py @@ -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 diff --git a/modules/logger.py b/modules/logger.py index 9db3aca..5d18811 100644 --- a/modules/logger.py +++ b/modules/logger.py @@ -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'{message}' + # 편의 메서드 추가 + 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) diff --git a/modules/login.py b/modules/login.py deleted file mode 100644 index ed16ec0..0000000 --- a/modules/login.py +++ /dev/null @@ -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() diff --git a/modules/login_page.py b/modules/login_page.py new file mode 100644 index 0000000..4191dbc --- /dev/null +++ b/modules/login_page.py @@ -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() diff --git a/modules/main_window.py b/modules/main_window.py deleted file mode 100644 index 251665e..0000000 --- a/modules/main_window.py +++ /dev/null @@ -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 diff --git a/modules/market_page.py b/modules/market_page.py new file mode 100644 index 0000000..bfcf608 --- /dev/null +++ b/modules/market_page.py @@ -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 diff --git a/modules/product_filter.py b/modules/product_filter.py index 631da71..96f2fd2 100644 --- a/modules/product_filter.py +++ b/modules/product_filter.py @@ -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 diff --git a/modules/product_page.py b/modules/product_page.py new file mode 100644 index 0000000..836bc9b --- /dev/null +++ b/modules/product_page.py @@ -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 diff --git a/modules/project_info.py b/modules/project_info.py new file mode 100644 index 0000000..7f404e8 --- /dev/null +++ b/modules/project_info.py @@ -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": ""} diff --git a/modules/setting_manager.py b/modules/setting_manager.py index 865d35a..1d0dbe1 100644 --- a/modules/setting_manager.py +++ b/modules/setting_manager.py @@ -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 {} diff --git a/poetry.lock b/poetry.lock index 80d0fe5..6dc4712 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index 071b988..7ae7002 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"}] diff --git a/tests/input_market.py b/tests/input_market.py new file mode 100644 index 0000000..464d912 --- /dev/null +++ b/tests/input_market.py @@ -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 ") + 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() diff --git a/user_settings.json b/user_settings.json new file mode 100644 index 0000000..947d2ec --- /dev/null +++ b/user_settings.json @@ -0,0 +1,4 @@ +{ + "username": "leensoo1nt@gmail.com", + "id": "909d2ef8-7053-4006-ab40-49eb49f20383" +} \ No newline at end of file