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 0000000..08d8f5e Binary files /dev/null and b/modules/__pycache__/app_manager.cpython-311.pyc differ diff --git a/modules/__pycache__/app_state.cpython-311.pyc b/modules/__pycache__/app_state.cpython-311.pyc new file mode 100644 index 0000000..587f087 Binary files /dev/null and b/modules/__pycache__/app_state.cpython-311.pyc differ diff --git a/modules/__pycache__/backend.cpython-311.pyc b/modules/__pycache__/backend.cpython-311.pyc index f3f14c1..b2e83be 100644 Binary files a/modules/__pycache__/backend.cpython-311.pyc and b/modules/__pycache__/backend.cpython-311.pyc differ diff --git a/modules/__pycache__/category_page.cpython-311.pyc b/modules/__pycache__/category_page.cpython-311.pyc new file mode 100644 index 0000000..3680378 Binary files /dev/null and b/modules/__pycache__/category_page.cpython-311.pyc differ diff --git a/modules/__pycache__/db_manager.cpython-311.pyc b/modules/__pycache__/db_manager.cpython-311.pyc index 2b7b751..f174f6d 100644 Binary files a/modules/__pycache__/db_manager.cpython-311.pyc and b/modules/__pycache__/db_manager.cpython-311.pyc differ diff --git a/modules/__pycache__/export.cpython-311.pyc b/modules/__pycache__/export.cpython-311.pyc index 556993b..c17ac1f 100644 Binary files a/modules/__pycache__/export.cpython-311.pyc and b/modules/__pycache__/export.cpython-311.pyc differ diff --git a/modules/__pycache__/forbidden_page.cpython-311.pyc b/modules/__pycache__/forbidden_page.cpython-311.pyc new file mode 100644 index 0000000..fa08596 Binary files /dev/null and b/modules/__pycache__/forbidden_page.cpython-311.pyc differ diff --git a/modules/__pycache__/logger.cpython-311.pyc b/modules/__pycache__/logger.cpython-311.pyc index be8f9c7..45cd330 100644 Binary files a/modules/__pycache__/logger.cpython-311.pyc and b/modules/__pycache__/logger.cpython-311.pyc differ diff --git a/modules/__pycache__/login_page.cpython-311.pyc b/modules/__pycache__/login_page.cpython-311.pyc new file mode 100644 index 0000000..fce25e8 Binary files /dev/null and b/modules/__pycache__/login_page.cpython-311.pyc differ diff --git a/modules/__pycache__/market_page.cpython-311.pyc b/modules/__pycache__/market_page.cpython-311.pyc new file mode 100644 index 0000000..d50a202 Binary files /dev/null and b/modules/__pycache__/market_page.cpython-311.pyc differ diff --git a/modules/__pycache__/product_filter.cpython-311.pyc b/modules/__pycache__/product_filter.cpython-311.pyc index 043ab33..a8f8b85 100644 Binary files a/modules/__pycache__/product_filter.cpython-311.pyc and b/modules/__pycache__/product_filter.cpython-311.pyc differ diff --git a/modules/__pycache__/product_page.cpython-311.pyc b/modules/__pycache__/product_page.cpython-311.pyc new file mode 100644 index 0000000..28e44c2 Binary files /dev/null and b/modules/__pycache__/product_page.cpython-311.pyc differ diff --git a/modules/__pycache__/project_info.cpython-311.pyc b/modules/__pycache__/project_info.cpython-311.pyc new file mode 100644 index 0000000..0cdca3f Binary files /dev/null and b/modules/__pycache__/project_info.cpython-311.pyc differ diff --git a/modules/__pycache__/setting_manager.cpython-311.pyc b/modules/__pycache__/setting_manager.cpython-311.pyc index ace6abf..5f877ca 100644 Binary files a/modules/__pycache__/setting_manager.cpython-311.pyc and b/modules/__pycache__/setting_manager.cpython-311.pyc differ 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