This commit is contained in:
Envy_PC 2025-03-30 08:29:07 +09:00
parent eb9c0834bb
commit ee77982bc7
34 changed files with 1259 additions and 580 deletions

200
app.log
View File

@ -37,3 +37,203 @@
[2025-03-28 09:32:26,373] [DEBUG] Exiting LoginDialog.create_dialog() [2025-03-28 09:32:26,373] [DEBUG] Exiting LoginDialog.create_dialog()
[2025-03-28 09:32:26,373] [DEBUG] LoginDialog.show() 호출됨 [2025-03-28 09:32:26,373] [DEBUG] LoginDialog.show() 호출됨
>>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494 >>>>>>> 20e76c4ca9e851ce44c679f9528e19eb53a8b494
[2025-03-28 12:32:42,342] [DEBUG] Initializing DBManager...
[2025-03-28 12:32:43,030] [DEBUG] DBManager initialized
[2025-03-28 12:32:43,031] [DEBUG] Initializing LoginPage...
[2025-03-28 12:32:45,175] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 12:32:45,176] [WARNING] 로그인 실패
[2025-03-28 12:32:48,280] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 12:32:48,281] [WARNING] 로그인 실패
[2025-03-28 12:37:16,267] [DEBUG] Logger initialized
[2025-03-28 12:37:16,268] [DEBUG] Initializing DBManager...
[2025-03-28 12:37:16,899] [DEBUG] DBManager initialized
[2025-03-28 12:37:16,900] [DEBUG] Initializing LoginPage...
[2025-03-28 12:37:56,645] [DEBUG] Logger initialized
[2025-03-28 12:37:56,646] [DEBUG] Initializing DBManager...
[2025-03-28 12:37:57,265] [DEBUG] DBManager initialized
[2025-03-28 12:37:57,266] [DEBUG] Initializing LoginPage...
[2025-03-28 12:38:26,308] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 12:38:26,309] [DEBUG] Entering DBManager.login()
[2025-03-28 12:38:26,535] [DEBUG] Entering update_client_with_token()
[2025-03-28 12:38:26,943] [DEBUG] Client updated with JWT token
[2025-03-28 12:38:26,945] [DEBUG] Exiting update_client_with_token()
[2025-03-28 12:38:26,945] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 12:38:26,946] [DEBUG] Exiting DBManager.login()
[2025-03-28 12:38:26,947] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 12:38:26,947] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
[2025-03-28 12:38:27,530] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
[2025-03-28 12:38:27,531] [DEBUG] Exiting update_last_login()
[2025-03-28 12:39:16,830] [DEBUG] Logger initialized
[2025-03-28 12:39:16,847] [DEBUG] Initializing DBManager...
[2025-03-28 12:39:17,461] [DEBUG] DBManager initialized
[2025-03-28 12:39:17,462] [DEBUG] Initializing LoginPage...
[2025-03-28 12:39:36,067] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 12:39:36,068] [DEBUG] Entering DBManager.login()
[2025-03-28 12:39:36,324] [DEBUG] Entering update_client_with_token()
[2025-03-28 12:39:36,762] [DEBUG] Client updated with JWT token
[2025-03-28 12:39:36,762] [DEBUG] Exiting update_client_with_token()
[2025-03-28 12:39:36,763] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 12:39:36,764] [DEBUG] Exiting DBManager.login()
[2025-03-28 12:39:36,765] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 12:39:36,765] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
[2025-03-28 12:39:37,390] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
[2025-03-28 12:39:37,391] [DEBUG] Exiting update_last_login()
[2025-03-28 12:39:37,393] [DEBUG] MarketPage initialized
[2025-03-28 12:41:53,495] [DEBUG] Logger initialized
[2025-03-28 12:41:53,499] [DEBUG] Initializing DBManager...
[2025-03-28 12:41:54,130] [DEBUG] DBManager initialized
[2025-03-28 12:41:54,131] [DEBUG] Initializing LoginPage...
[2025-03-28 12:42:25,102] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 12:42:25,103] [DEBUG] Entering DBManager.login()
[2025-03-28 12:42:25,242] [DEBUG] Entering update_client_with_token()
[2025-03-28 12:42:25,649] [DEBUG] Client updated with JWT token
[2025-03-28 12:42:25,650] [DEBUG] Exiting update_client_with_token()
[2025-03-28 12:42:25,651] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 12:42:25,651] [DEBUG] Exiting DBManager.login()
[2025-03-28 12:42:25,652] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 12:42:25,653] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
[2025-03-28 12:42:26,193] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
[2025-03-28 12:42:26,194] [DEBUG] Exiting update_last_login()
[2025-03-28 12:42:26,196] [DEBUG] MarketPage initialized
[2025-03-28 12:42:26,200] [DEBUG] MarketPage.load_market_list() 호출됨
[2025-03-28 12:42:26,200] [DEBUG] Entering get_market_list()
[2025-03-28 12:42:27,201] [DEBUG] Market list retrieved: [{'name': 'Market A', 'url': 'https://market-a.com', 'memo': 'Memo A'}, {'name': 'Market B', 'url': 'https://market-b.com', 'memo': 'Memo B'}]
[2025-03-28 12:42:27,202] [DEBUG] Exiting get_market_list()
[2025-03-28 12:45:05,179] [DEBUG] Logger initialized
[2025-03-28 12:45:05,179] [DEBUG] Initializing DBManager...
[2025-03-28 12:45:05,807] [DEBUG] DBManager initialized
[2025-03-28 12:45:05,808] [DEBUG] Initializing LoginPage...
[2025-03-28 12:48:32,757] [DEBUG] Logger initialized
[2025-03-28 12:48:32,759] [DEBUG] Initializing DBManager...
[2025-03-28 12:48:33,378] [DEBUG] DBManager initialized
[2025-03-28 12:48:33,379] [DEBUG] Initializing LoginPage...
[2025-03-28 12:49:14,007] [DEBUG] Logger initialized
[2025-03-28 12:49:14,010] [DEBUG] Initializing DBManager...
[2025-03-28 12:49:14,627] [DEBUG] DBManager initialized
[2025-03-28 12:49:14,629] [DEBUG] Initializing LoginPage...
[2025-03-28 12:50:06,883] [DEBUG] Logger initialized
[2025-03-28 12:50:06,885] [DEBUG] Initializing DBManager...
[2025-03-28 12:50:07,517] [DEBUG] DBManager initialized
[2025-03-28 12:50:07,517] [DEBUG] Initializing LoginPage...
[2025-03-28 12:51:29,299] [DEBUG] Logger initialized
[2025-03-28 12:51:29,300] [DEBUG] Initializing DBManager...
[2025-03-28 12:51:29,950] [DEBUG] DBManager initialized
[2025-03-28 12:51:29,951] [DEBUG] Initializing LoginPage...
[2025-03-28 13:02:53,368] [DEBUG] Logger initialized
[2025-03-28 13:02:53,369] [DEBUG] Initializing DBManager...
[2025-03-28 13:02:54,057] [DEBUG] DBManager initialized
[2025-03-28 13:02:54,057] [DEBUG] Initializing LoginPage...
[2025-03-28 13:04:46,339] [DEBUG] Logger initialized
[2025-03-28 13:04:46,341] [DEBUG] Initializing DBManager...
[2025-03-28 13:04:46,958] [DEBUG] DBManager initialized
[2025-03-28 13:04:46,959] [DEBUG] Initializing LoginPage...
[2025-03-28 13:05:53,638] [DEBUG] Logger initialized
[2025-03-28 13:05:53,640] [DEBUG] Initializing DBManager...
[2025-03-28 13:05:54,324] [DEBUG] DBManager initialized
[2025-03-28 13:05:54,324] [DEBUG] Initializing LoginPage...
[2025-03-28 13:06:05,877] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 13:06:05,878] [DEBUG] Entering DBManager.login()
[2025-03-28 13:06:06,167] [DEBUG] Entering update_client_with_token()
[2025-03-28 13:06:06,596] [DEBUG] Client updated with JWT token
[2025-03-28 13:06:06,597] [DEBUG] Exiting update_client_with_token()
[2025-03-28 13:06:06,598] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 13:06:06,598] [DEBUG] Exiting DBManager.login()
[2025-03-28 13:06:06,599] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 13:06:06,599] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
[2025-03-28 13:06:07,284] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
[2025-03-28 13:06:07,284] [DEBUG] Exiting update_last_login()
[2025-03-28 13:06:07,288] [DEBUG] MarketPage initialized
[2025-03-28 13:06:07,292] [DEBUG] MarketPage.load_market_list() 호출됨
[2025-03-28 13:06:07,293] [DEBUG] Entering get_market_list()
[2025-03-28 13:06:08,295] [DEBUG] Market list retrieved: [{'name': 'Market A', 'url': 'https://market-a.com', 'memo': 'Memo A'}, {'name': 'Market B', 'url': 'https://market-b.com', 'memo': 'Memo B'}]
[2025-03-28 13:06:08,296] [DEBUG] Exiting get_market_list()
[2025-03-28 13:08:43,321] [DEBUG] Logger initialized
[2025-03-28 13:08:43,324] [DEBUG] Initializing DBManager...
[2025-03-28 13:08:43,952] [DEBUG] DBManager initialized
[2025-03-28 13:08:43,953] [DEBUG] Initializing LoginPage...
[2025-03-28 13:13:09,363] [DEBUG] Logger initialized
[2025-03-28 13:13:09,365] [DEBUG] Initializing DBManager...
[2025-03-28 13:13:09,992] [DEBUG] DBManager initialized
[2025-03-28 13:13:46,986] [DEBUG] Logger initialized
[2025-03-28 13:13:46,987] [DEBUG] Initializing DBManager...
[2025-03-28 13:13:47,623] [DEBUG] DBManager initialized
[2025-03-28 13:13:47,623] [DEBUG] Initializing LoginPage...
[2025-03-28 13:15:37,273] [DEBUG] Logger initialized
[2025-03-28 13:15:37,274] [DEBUG] Initializing DBManager...
[2025-03-28 13:15:37,947] [DEBUG] DBManager initialized
[2025-03-28 13:15:37,947] [DEBUG] Initializing LoginPage...
[2025-03-28 13:16:14,265] [DEBUG] Logger initialized
[2025-03-28 13:16:14,265] [DEBUG] Initializing DBManager...
[2025-03-28 13:16:14,738] [DEBUG] DBManager initialized
[2025-03-28 13:16:14,739] [DEBUG] Initializing LoginPage...
[2025-03-28 13:17:11,858] [DEBUG] Logger initialized
[2025-03-28 13:17:11,858] [DEBUG] Initializing DBManager...
[2025-03-28 13:17:12,481] [DEBUG] DBManager initialized
[2025-03-28 13:17:12,481] [DEBUG] Initializing LoginPage...
[2025-03-28 13:22:45,886] [DEBUG] Logger initialized
[2025-03-28 13:22:45,887] [DEBUG] Initializing DBManager...
[2025-03-28 13:22:46,514] [DEBUG] DBManager initialized
[2025-03-28 13:22:46,514] [DEBUG] Initializing LoginPage...
[2025-03-28 13:23:10,131] [DEBUG] Logger initialized
[2025-03-28 13:23:10,132] [DEBUG] Initializing DBManager...
[2025-03-28 13:23:10,767] [DEBUG] DBManager initialized
[2025-03-28 13:23:10,768] [DEBUG] Initializing LoginPage...
[2025-03-28 13:23:26,381] [DEBUG] Logger initialized
[2025-03-28 13:23:26,382] [DEBUG] Initializing DBManager...
[2025-03-28 13:23:27,004] [DEBUG] DBManager initialized
[2025-03-28 13:23:27,005] [DEBUG] Initializing LoginPage...
[2025-03-28 13:23:42,158] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 13:23:42,159] [DEBUG] Entering DBManager.login()
[2025-03-28 13:23:42,345] [DEBUG] Entering update_client_with_token()
[2025-03-28 13:23:42,777] [DEBUG] Client updated with JWT token
[2025-03-28 13:23:42,778] [DEBUG] Exiting update_client_with_token()
[2025-03-28 13:23:42,778] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 13:23:42,779] [DEBUG] Exiting DBManager.login()
[2025-03-28 13:23:42,780] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 13:23:42,780] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
[2025-03-28 13:23:43,582] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
[2025-03-28 13:23:43,582] [DEBUG] Exiting update_last_login()
[2025-03-28 13:23:43,586] [DEBUG] MarketPage initialized
[2025-03-28 13:23:43,589] [DEBUG] MarketPage.load_market_list() 호출됨
[2025-03-28 13:23:43,590] [DEBUG] Entering get_market_list()
[2025-03-28 13:23:44,591] [DEBUG] Market list retrieved: [{'name': 'Market A', 'url': 'https://market-a.com', 'memo': 'Memo A'}, {'name': 'Market B', 'url': 'https://market-b.com', 'memo': 'Memo B'}]
[2025-03-28 13:23:44,593] [DEBUG] Exiting get_market_list()
[2025-03-28 13:59:40,064] [DEBUG] Logger initialized
[2025-03-28 13:59:40,064] [DEBUG] Initializing DBManager...
[2025-03-28 13:59:40,460] [DEBUG] DBManager initialized
[2025-03-28 13:59:40,460] [DEBUG] Initializing LoginPage...
[2025-03-28 13:59:40,462] [DEBUG] AppManager.start() 호출됨 - 로그인 페이지 표시
[2025-03-28 14:14:14,474] [DEBUG] Logger initialized
[2025-03-28 14:15:19,450] [DEBUG] Logger initialized
[2025-03-28 14:15:19,452] [DEBUG] Initializing DBManager...
[2025-03-28 14:15:19,802] [DEBUG] DBManager initialized
[2025-03-28 14:15:19,803] [DEBUG] AppManager.start() - 시작, 로그인 페이지 표시
[2025-03-28 14:15:19,803] [DEBUG] Initializing LoginPage...
[2025-03-28 14:15:33,083] [DEBUG] LoginPage.login() 호출됨
[2025-03-28 14:15:33,084] [DEBUG] Entering DBManager.login()
[2025-03-28 14:15:33,251] [DEBUG] Entering update_client_with_token()
[2025-03-28 14:15:33,502] [DEBUG] Client updated with JWT token
[2025-03-28 14:15:33,503] [DEBUG] Exiting update_client_with_token()
[2025-03-28 14:15:33,503] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 14:15:33,503] [DEBUG] Exiting DBManager.login()
[2025-03-28 14:15:33,504] [DEBUG] 로그인 성공: {'id': '909d2ef8-7053-4006-ab40-49eb49f20383', 'email': 'leensoo1nt@gmail.com', 'nickname': 'Unknown'}
[2025-03-28 14:15:33,504] [DEBUG] Entering update_last_login(user_id=909d2ef8-7053-4006-ab40-49eb49f20383)
[2025-03-28 14:15:33,891] [INFO] Last login updated for user 909d2ef8-7053-4006-ab40-49eb49f20383
[2025-03-28 14:15:33,892] [DEBUG] Exiting update_last_login()
[2025-03-28 14:15:33,894] [DEBUG] AppManager.on_login_success() 호출됨
[2025-03-28 14:15:33,894] [DEBUG] AppManager.show_main_ui() 호출됨
[2025-03-28 14:15:33,895] [DEBUG] Building MarketPage content
[2025-03-28 14:15:33,896] [DEBUG] MarketPage.load_market_list() 호출됨
[2025-03-28 14:15:33,896] [DEBUG] Entering DBManager.get_markets()
[2025-03-28 14:15:33,962] [ERROR] get_markets 에러: {'code': '42P01', 'details': None, 'hint': None, 'message': 'relation "public.markets" does not exist'}
Traceback (most recent call last):
File "D:\py\Resell1\modules\db_manager.py", line 72, in get_markets
response = self.client.table("markets").select("*").execute()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "D:\py\Resell1\.venv\Lib\site-packages\postgrest\_sync\request_builder.py", line 78, in execute
raise APIError(r.json())
postgrest.exceptions.APIError: {'code': '42P01', 'details': None, 'hint': None, 'message': 'relation "public.markets" does not exist'}
[2025-03-28 14:15:33,963] [DEBUG] Building ProductPage content
[2025-03-28 14:15:33,964] [DEBUG] ForbiddenPage initialized
[2025-03-28 14:15:33,965] [DEBUG] CategoryPage initialized

195
main.py
View File

@ -1,196 +1,9 @@
# main.py
import flet as ft import flet as ft
from modules import logger, login from modules.app_manager import AppManager
from modules.setting_manager import SettingsManager
from modules.db_manager import DBManager
from modules.main_window import MainWindow
def main(page: ft.Page): def main(page: ft.Page):
page.title = "Modern Market and Product Manager" app_manager = AppManager(page)
page.window_width = 1000 app_manager.start()
page.window_height = 700
# 하단 로그 출력용 텍스트 위젯
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)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

106
modules/app_manager.py Normal file
View File

@ -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()

17
modules/app_state.py Normal file
View File

@ -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

18
modules/category_page.py Normal file
View File

@ -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

View File

@ -6,10 +6,6 @@ import logging
import traceback import traceback
class DBManager: class DBManager:
"""
DBManager는 Supabase 클라이언트를 래핑하여 로그인, 사용자 정보 조회,
마지막 로그인 시간 업데이트 여러 API 호출을 수행합니다.
"""
def __init__(self, logger: logging.Logger): def __init__(self, logger: logging.Logger):
self.logger = logger self.logger = logger
self.logger.log("Initializing DBManager...", level=logging.DEBUG) 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) self.logger.log(f"Login error: {e}", level=logging.ERROR, exc_info=True)
return {"error": str(e)} 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): def update_last_login(self, user_id: str):
self.logger.log(f"Entering update_last_login(user_id={user_id})", level=logging.DEBUG) self.logger.log(f"Entering update_last_login(user_id={user_id})", level=logging.DEBUG)
try: try:
@ -98,3 +62,17 @@ class DBManager:
except Exception as e: except Exception as e:
self.logger.log(f"update_last_login 에러: {e}", level=logging.ERROR, exc_info=True) self.logger.log(f"update_last_login 에러: {e}", level=logging.ERROR, exc_info=True)
self.logger.log("Exiting update_last_login()", level=logging.DEBUG) 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 []

18
modules/forbidden_page.py Normal file
View File

@ -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

View File

@ -5,47 +5,44 @@ import traceback
class Logger: class Logger:
def __init__(self, gui_callback=None, log_file="app.log", logger_name="FletLogger", level=logging.DEBUG): 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.gui_callback = gui_callback
self.logger = logging.getLogger(logger_name) self.logger = logging.getLogger(logger_name)
self.logger.setLevel(level) self.logger.setLevel(level)
# 파일 핸들러 설정 (RotatingFileHandler) # 파일 핸들러 (최대 10MB, 5회 백업)
file_handler = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5, encoding="utf-8") file_handler = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5, encoding="utf-8")
file_handler.setLevel(level) file_handler.setLevel(level)
formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s") formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
file_handler.setFormatter(formatter) file_handler.setFormatter(formatter)
self.logger.addHandler(file_handler) self.logger.addHandler(file_handler)
# 콘솔 핸들러 설정 # 콘솔 핸들러
console_handler = logging.StreamHandler() console_handler = logging.StreamHandler()
console_handler.setLevel(level) console_handler.setLevel(level)
console_handler.setFormatter(formatter) console_handler.setFormatter(formatter)
self.logger.addHandler(console_handler) self.logger.addHandler(console_handler)
self.log("Logger initialized", level=logging.DEBUG)
def log(self, message, level=logging.INFO, exc_info=False): def log(self, message, level=logging.INFO, exc_info=False):
"""
메시지를 기록하고, GUI 콜백이 있다면 레벨별 색상이 적용된 HTML 문자열을 전달합니다.
"""
if exc_info: if exc_info:
message += "\n" + traceback.format_exc() message += "\n" + traceback.format_exc()
self.logger.log(level, message) self.logger.log(level, message)
if self.gui_callback: if self.gui_callback:
formatted_message = self.format_gui_message(message, level) # HTML 태그 없이 순수 텍스트를 전달합니다.
self.gui_callback(formatted_message) self.gui_callback(message)
def format_gui_message(self, message, level): # 편의 메서드 추가
""" def debug(self, message):
레벨별 색상을 적용한 HTML 문자열 반환. self.log(message, level=logging.DEBUG)
"""
color_map = { def info(self, message):
logging.DEBUG: "gray", self.log(message, level=logging.INFO)
logging.INFO: "black",
logging.WARNING: "orange", def warning(self, message):
logging.ERROR: "red", self.log(message, level=logging.WARNING)
logging.CRITICAL: "purple"
} def error(self, message):
color = color_map.get(level, "black") self.log(message, level=logging.ERROR)
return f'<span style="color:{color};">{message}</span>'
def get_logger(gui_callback=None): def get_logger(gui_callback=None):
return Logger(gui_callback=gui_callback) return Logger(gui_callback=gui_callback)

View File

@ -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()

126
modules/login_page.py Normal file
View File

@ -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()

View File

@ -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

75
modules/market_page.py Normal file
View File

@ -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

View File

@ -3,10 +3,7 @@ def filter_forbidden_words(products):
logger = logging.getLogger("FletLogger") logger = logging.getLogger("FletLogger")
logger.debug("Entering filter_forbidden_words()") logger.debug("Entering filter_forbidden_words()")
forbidden_words = ["bad", "illegal", "금지어"] forbidden_words = ["bad", "illegal", "금지어"]
filtered = [] filtered = [p for p in products if not any(word in p.get("name", "").lower() for word in forbidden_words)]
for product in products:
if not any(word in product.get("name", "").lower() for word in forbidden_words):
filtered.append(product)
logger.debug(f"Filtered products (forbidden words): {filtered}") logger.debug(f"Filtered products (forbidden words): {filtered}")
logger.debug("Exiting filter_forbidden_words()") logger.debug("Exiting filter_forbidden_words()")
return filtered return filtered
@ -16,10 +13,7 @@ def filter_forbidden_categories(products):
logger = logging.getLogger("FletLogger") logger = logging.getLogger("FletLogger")
logger.debug("Entering filter_forbidden_categories()") logger.debug("Entering filter_forbidden_categories()")
forbidden_categories = ["Forbidden Category"] forbidden_categories = ["Forbidden Category"]
filtered = [] filtered = [p for p in products if p.get("category", "") not in forbidden_categories]
for product in products:
if product.get("category", "") not in forbidden_categories:
filtered.append(product)
logger.debug(f"Filtered products (forbidden categories): {filtered}") logger.debug(f"Filtered products (forbidden categories): {filtered}")
logger.debug("Exiting filter_forbidden_categories()") logger.debug("Exiting filter_forbidden_categories()")
return filtered return filtered

89
modules/product_page.py Normal file
View File

@ -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

23
modules/project_info.py Normal file
View File

@ -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": ""}

View File

@ -1,43 +1,52 @@
import keyring
import json import json
import os import os
class SettingsManager: class SettingsManager:
""" def __init__(self, project_info):
로컬 JSON 파일("settings.json") 이용해 사용자 설정(로그인 정보, GUI 설정 ) 저장/불러옵니다. # project_info의 'name'을 보안 저장소의 SERVICE_NAME으로 사용
""" self.service_name = project_info.get("name", "App")
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 save_user_info(self, user_info: dict): def save_user_info(self, user_info: dict):
self.settings["user"] = user_info email = user_info.get("email", "")
self.save_settings() 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: 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): def save_gui_settings(self, gui_settings: dict):
self.settings["gui"] = gui_settings try:
self.save_settings() 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: 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 {}

368
poetry.lock generated
View File

@ -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 = ["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\""] 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]] [[package]]
name = "certifi" name = "certifi"
version = "2025.1.31" version = "2025.1.31"
@ -221,6 +238,87 @@ files = [
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, {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]] [[package]]
name = "charset-normalizer" name = "charset-normalizer"
version = "3.4.1" version = "3.4.1"
@ -336,6 +434,65 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {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]] [[package]]
name = "deprecation" name = "deprecation"
version = "2.1.0" version = "2.1.0"
@ -620,6 +777,31 @@ files = [
[package.extras] [package.extras]
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 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]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.1.0" version = "2.1.0"
@ -632,6 +814,114 @@ files = [
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, {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]] [[package]]
name = "lxml" name = "lxml"
version = "5.3.1" version = "5.3.1"
@ -788,6 +1078,18 @@ html5 = ["html5lib"]
htmlsoup = ["BeautifulSoup4"] htmlsoup = ["BeautifulSoup4"]
source = ["Cython (>=3.0.11,<3.1.0)"] 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]] [[package]]
name = "multidict" name = "multidict"
version = "6.2.0" 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"] 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"] 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]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.10.6" version = "2.10.6"
@ -1497,6 +1812,19 @@ files = [
{file = "pywin32-310-cp39-cp39-win_amd64.whl", hash = "sha256:96867217335559ac619f00ad70e513c0fcf84b8a3af9fc2bba3b59b97da70475"}, {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]] [[package]]
name = "realtime" name = "realtime"
version = "2.4.2" version = "2.4.2"
@ -1552,6 +1880,23 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"] socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 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]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.17.0"
@ -1925,7 +2270,28 @@ idna = ">=2.0"
multidict = ">=4.0" multidict = ">=4.0"
propcache = ">=0.2.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] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.11,<4.0" python-versions = ">=3.11,<4.0"
content-hash = "acbb4a6d39e2aff7e5bb4664bd9fc4bb0c8d00809d067b7016e98584f8ebec3d" content-hash = "a72b2533fbfa496663693cb7f0429d8c6f27e4f552e38580d14fb86eed879d33"

View File

@ -1,7 +1,7 @@
[project] [project]
name = "Resell1" name = "Resell1"
version = "1.0.0" version = "1.0.0"
description = "나도 좀 팔자" description = "나도 좀 같이 아보는 팔판 소싱기"
authors = [ authors = [
{name = "WhenRideMyCar",email = "kkebiini@gmail.com"} {name = "WhenRideMyCar",email = "kkebiini@gmail.com"}
] ]
@ -13,8 +13,10 @@ dependencies = [
"openpyxl (>=3.1.5,<4.0.0)", "openpyxl (>=3.1.5,<4.0.0)",
"xlwings (>=0.33.11,<0.34.0)", "xlwings (>=0.33.11,<0.34.0)",
"requests (>=2.32.3,<3.0.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] [tool.poetry]
packages = [{include = "resell1", from = "src"}] packages = [{include = "resell1", from = "src"}]

133
tests/input_market.py Normal file
View File

@ -0,0 +1,133 @@
import asyncio
import re
import sys
from bs4 import BeautifulSoup
from playwright.sync_api import sync_playwright
from supabase import create_client, Client
import getpass
# HTML 파일에서 스마트스토어 URL 추출 함수
def extract_market_urls(html_file_path):
with open(html_file_path, "r", encoding="utf-8") as f:
html_content = f.read()
soup = BeautifulSoup(html_content, "html.parser")
links = soup.find_all("a", href=True)
market_urls = []
for link in links:
href = link["href"]
if href.startswith("https://smartstore.naver.com"):
market_urls.append(href)
# 중복 제거
return list(set(market_urls))
# Playwright를 사용하여 마켓 정보 수집 함수
def fetch_market_info(url):
# 기본값 설정
market_name = ""
market_grade = ""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
try:
page.goto(url, timeout=60000) # 60초 timeout
page.wait_for_load_state("domcontentloaded", timeout=60000)
# market_name 추출
try:
name_elem = page.query_selector("div#pc-storeNameWidget span")
if name_elem:
market_name = name_elem.inner_text().strip()
except Exception as e:
print(f"Error fetching market_name from {url}: {e}")
# market_grade 추출
try:
grade_elem = page.query_selector("div#pc-sellerInfoWidget div > div > div > div:nth-child(1)")
if grade_elem:
spans = grade_elem.query_selector_all("span")
if len(spans) >= 3:
market_grade = spans[2].inner_text().strip()
else:
market_grade = grade_elem.inner_text().strip()
except Exception as e:
print(f"Error fetching market_grade from {url}: {e}")
except Exception as e:
print(f"Error loading page {url}: {e}")
finally:
browser.close()
return market_name, market_grade
# Supabase 로그인 및 데이터 삽입 함수 (market_url 중복 검사 추가)
def supabase_insert_markets(supabase_url: str, supabase_key: str, market_data: list):
"""
market_data: 리스트로 [(market_url, market_name, market_grade), ...]
"""
supabase: Client = create_client(supabase_url, supabase_key)
for url, name, grade in market_data:
# 중복 검사: market_url이 이미 존재하는지 확인
existing = supabase.table("markets").select("*").eq("market_url", url).execute()
if existing.data:
print(f"{url} 은(는) 이미 존재합니다. 건너뜁니다.")
continue
data = {
"market_name": name,
"market_url": url,
"market_grade": grade,
"market_memo": ""
}
try:
response = supabase.table("markets").insert(data).execute()
if response.get("error"):
print(f"Failed to insert {url}: {response['error']['message']}")
else:
print(f"Inserted {url} successfully.")
except Exception as e:
print(f"Exception inserting {url}: {e}")
def main():
if len(sys.argv) < 2:
print("Usage: python module.py <html_file_path>")
sys.exit(1)
html_file_path = sys.argv[1]
# 1. HTML 파일에서 마켓 URL 추출
market_urls = extract_market_urls(html_file_path)
print(f"{len(market_urls)}개의 스마트스토어 URL을 찾았습니다.")
# 2. 각 URL에 대해 Playwright로 정보 수집
market_data = []
for url in market_urls:
print(f"Processing {url} ...")
name, grade = fetch_market_info(url)
print(f" market_name: {name}")
print(f" market_grade: {grade}")
market_data.append((url, name, grade))
# 3. Supabase 자격 증명 입력받기
print("Supabase 로그인 정보를 입력하세요.")
supabase_url = input("Supabase URL: ").strip()
supabase_id = input("Supabase Email (ID): ").strip()
supabase_pw = getpass.getpass("Supabase Password: ").strip()
# Supabase 클라이언트 생성 및 로그인 (실제 환경에 따라 인증 방식이 다를 수 있음)
supabase: Client = create_client(supabase_url, supabase_pw)
try:
auth_response = supabase.auth.sign_in(email=supabase_id, password=supabase_pw)
if auth_response.get("error"):
print(f"Supabase 로그인 실패: {auth_response['error']['message']}")
sys.exit(1)
else:
print("Supabase 로그인 성공!")
except Exception as e:
print(f"Supabase 로그인 예외: {e}")
sys.exit(1)
# 4. 수집한 데이터 Supabase에 삽입 (중복 검사 포함)
supabase_insert_markets(supabase_url, supabase_pw, market_data)
if __name__ == "__main__":
main()

4
user_settings.json Normal file
View File

@ -0,0 +1,4 @@
{
"username": "leensoo1nt@gmail.com",
"id": "909d2ef8-7053-4006-ab40-49eb49f20383"
}