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