first commit
This commit is contained in:
commit
1dc97cafd7
|
|
@ -0,0 +1 @@
|
|||
.venv/
|
||||
|
|
@ -0,0 +1,203 @@
|
|||
import flet as ft
|
||||
from modules import logger, login, backend, product_filter, export
|
||||
from modules.setting_manager import SettingsManager
|
||||
from modules.db_manager import DBeManager
|
||||
import logging
|
||||
|
||||
# 전역 변수 (데모용 데이터 저장)
|
||||
market_list = []
|
||||
sold_products = []
|
||||
filtered_products = []
|
||||
sourced_products = []
|
||||
|
||||
def main(page: ft.Page):
|
||||
page.title = "Modern Market and Product Manager"
|
||||
page.window_width = 1000
|
||||
page.window_height = 700
|
||||
|
||||
# 하단 로그 출력용 텍스트 위젯
|
||||
log_display = ft.Text(value="", size=12)
|
||||
def gui_log_callback(formatted_message: str):
|
||||
log_display.value += formatted_message + "\n"
|
||||
page.update()
|
||||
|
||||
# 로거, 설정 관리자, SupabaseManager 초기화
|
||||
app_logger = logger.get_logger(gui_callback=gui_log_callback)
|
||||
settings_manager = SettingsManager()
|
||||
supabase_manager = DBeManager(app_logger)
|
||||
|
||||
# 로그인 다이얼로그 실행 (비동기)
|
||||
async def do_login():
|
||||
login_dialog = login.LoginDialog(page, app_logger, settings_manager, supabase_manager)
|
||||
logged_in = await login_dialog.show()
|
||||
if logged_in:
|
||||
page.controls.clear()
|
||||
page.add(ft.Text("로그인 성공! 메인 화면입니다."), log_display)
|
||||
else:
|
||||
page.add(ft.Text("로그인 실패!"), log_display)
|
||||
page.async_run(do_login)
|
||||
|
||||
# 마켓 탭 UI 구성
|
||||
market_tab_content = ft.Column([
|
||||
ft.Row([
|
||||
ft.ElevatedButton("마켓목록 가져오기", on_click=lambda e: load_market_list(page)),
|
||||
ft.ElevatedButton("팔린상품 가져오기", on_click=lambda e: load_sold_products(page)),
|
||||
ft.ElevatedButton("마켓추가하기", on_click=lambda e: add_market(page))
|
||||
]),
|
||||
ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("마켓이름")),
|
||||
ft.DataColumn(ft.Text("마켓 URL")),
|
||||
ft.DataColumn(ft.Text("메모"))
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="market_table"
|
||||
)
|
||||
], scroll=ft.ScrollMode.AUTO)
|
||||
|
||||
# 상품 탭 UI 구성
|
||||
product_tab_content = ft.Column([
|
||||
ft.Row([
|
||||
ft.ElevatedButton("금지어필터링", on_click=lambda e: filter_forbidden(page)),
|
||||
ft.ElevatedButton("카테고리 필터링", on_click=lambda e: filter_category(page)),
|
||||
ft.Dropdown(
|
||||
label="소싱몰 목록",
|
||||
options=[
|
||||
ft.dropdown.Option("타오바오"),
|
||||
ft.dropdown.Option("1688")
|
||||
],
|
||||
key="sourcing_market"
|
||||
),
|
||||
ft.ElevatedButton("소싱하기", on_click=lambda e: sourcing_products(page)),
|
||||
ft.ElevatedButton("출력", on_click=lambda e: export_products(page))
|
||||
]),
|
||||
ft.DataTable(
|
||||
columns=[
|
||||
ft.DataColumn(ft.Text("상품명")),
|
||||
ft.DataColumn(ft.Text("카테고리")),
|
||||
ft.DataColumn(ft.Text("이미지 URL")),
|
||||
ft.DataColumn(ft.Text("소싱 URL"))
|
||||
],
|
||||
rows=[],
|
||||
expand=True,
|
||||
key="product_table"
|
||||
)
|
||||
], scroll=ft.ScrollMode.AUTO)
|
||||
|
||||
# 금지어 관리 탭 (추후 구현)
|
||||
forbidden_tab_content = ft.Column([
|
||||
ft.Text("금지어 관리 탭 내용 (추후 구현)")
|
||||
])
|
||||
|
||||
# 카테고리 관리 탭 (추후 구현)
|
||||
category_tab_content = ft.Column([
|
||||
ft.Text("카테고리 관리 탭 내용 (추후 구현)")
|
||||
])
|
||||
|
||||
# 메인 탭 생성
|
||||
tabs = ft.Tabs(
|
||||
selected_index=0,
|
||||
tabs=[
|
||||
ft.Tab(text="마켓", content=market_tab_content),
|
||||
ft.Tab(text="상품", content=product_tab_content),
|
||||
ft.Tab(text="금지어 관리", content=forbidden_tab_content),
|
||||
ft.Tab(text="카테고리 관리", content=category_tab_content)
|
||||
],
|
||||
key="main_tabs"
|
||||
)
|
||||
|
||||
# 페이지 레이아웃 구성
|
||||
page.add(tabs, log_display)
|
||||
|
||||
# 로그 추가 함수 (각 모듈에서 호출 가능하도록 page.session에 저장)
|
||||
def append_log(message: str):
|
||||
current = log_display.value
|
||||
log_display.value = current + message + "\n"
|
||||
page.update()
|
||||
page.session.set("append_log", append_log)
|
||||
|
||||
def load_market_list(page: ft.Page):
|
||||
global market_list
|
||||
page.session.get("append_log")("Fetching market list...")
|
||||
market_list = backend.get_market_list()
|
||||
market_rows = []
|
||||
for m in market_list:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(m.get("name", ""))),
|
||||
ft.DataCell(ft.Text(m.get("url", ""))),
|
||||
ft.DataCell(ft.Text(m.get("memo", "")))
|
||||
])
|
||||
market_rows.append(row)
|
||||
market_table: ft.DataTable = page.get_control("market_table")
|
||||
market_table.rows = market_rows
|
||||
page.session.get("append_log")("Market list loaded.")
|
||||
page.update()
|
||||
|
||||
def load_sold_products(page: ft.Page):
|
||||
global sold_products, filtered_products, sourced_products
|
||||
page.session.get("append_log")("Fetching sold products for each market...")
|
||||
sold_products = backend.get_sold_products(market_list)
|
||||
filtered_products = sold_products.copy()
|
||||
page.session.get("append_log")("Sold products loaded. Switching to 상품 탭.")
|
||||
update_product_table(page, filtered_products)
|
||||
tabs: ft.Tabs = page.get_control("main_tabs")
|
||||
tabs.selected_index = 1
|
||||
page.update()
|
||||
|
||||
def update_product_table(page: ft.Page, products):
|
||||
product_rows = []
|
||||
for p in products:
|
||||
row = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(p.get("name", ""))),
|
||||
ft.DataCell(ft.Text(p.get("category", ""))),
|
||||
ft.DataCell(ft.Text(p.get("image_url", ""))),
|
||||
ft.DataCell(ft.Text(p.get("sourcing_url", "")))
|
||||
])
|
||||
product_rows.append(row)
|
||||
product_table: ft.DataTable = page.get_control("product_table")
|
||||
product_table.rows = product_rows
|
||||
page.update()
|
||||
|
||||
def add_market(page: ft.Page):
|
||||
page.session.get("append_log")("Add market functionality not implemented yet.")
|
||||
page.update()
|
||||
|
||||
def filter_forbidden(page: ft.Page):
|
||||
global filtered_products
|
||||
page.session.get("append_log")("Filtering products with forbidden words...")
|
||||
filtered_products = product_filter.filter_forbidden_words(filtered_products)
|
||||
update_product_table(page, filtered_products)
|
||||
page.session.get("append_log")("Forbidden words filtering applied.")
|
||||
page.update()
|
||||
|
||||
def filter_category(page: ft.Page):
|
||||
global filtered_products
|
||||
page.session.get("append_log")("Filtering products with forbidden categories...")
|
||||
filtered_products = product_filter.filter_forbidden_categories(filtered_products)
|
||||
update_product_table(page, filtered_products)
|
||||
page.session.get("append_log")("Category filtering applied.")
|
||||
page.update()
|
||||
|
||||
def sourcing_products(page: ft.Page):
|
||||
global sourced_products, filtered_products
|
||||
sourcing_market: ft.Dropdown = page.get_control("sourcing_market")
|
||||
selected_market = sourcing_market.value
|
||||
page.session.get("append_log")(f"Starting sourcing using {selected_market}...")
|
||||
sourced_products = []
|
||||
for product in filtered_products:
|
||||
sourcing_url = backend.sourcing_product(product.get("image_url", ""), selected_market)
|
||||
product["sourcing_url"] = sourcing_url
|
||||
sourced_products.append(product)
|
||||
update_product_table(page, sourced_products)
|
||||
page.session.get("append_log")("Sourcing completed.")
|
||||
page.update()
|
||||
|
||||
def export_products(page: ft.Page):
|
||||
page.session.get("append_log")("Exporting products to Excel...")
|
||||
export.export_to_excel(sourced_products)
|
||||
page.session.get("append_log")("Products exported and folder opened.")
|
||||
page.update()
|
||||
|
||||
if __name__ == "__main__":
|
||||
ft.app(target=main)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,40 @@
|
|||
import time
|
||||
import random
|
||||
import requests
|
||||
from modules import logger
|
||||
|
||||
app_logger = logger.get_logger()
|
||||
|
||||
def get_market_list():
|
||||
# Supabase에서 마켓 목록을 가져오는 기능 (시뮬레이션)
|
||||
app_logger.info("Fetching market list from supabase...")
|
||||
# 데모용 더미 데이터
|
||||
time.sleep(1)
|
||||
return [
|
||||
{"name": "Market A", "url": "https://market-a.com", "memo": "Memo A"},
|
||||
{"name": "Market B", "url": "https://market-b.com", "memo": "Memo B"},
|
||||
]
|
||||
|
||||
def get_sold_products(markets):
|
||||
# 각 마켓의 판매 상품 목록을 가져오는 기능 (시뮬레이션)
|
||||
app_logger.info("Fetching sold products from markets...")
|
||||
products = []
|
||||
for market in markets:
|
||||
time.sleep(0.5) # 네트워크 딜레이 시뮬레이션
|
||||
# 더미 상품 데이터
|
||||
for i in range(3):
|
||||
products.append({
|
||||
"name": f"Product {i} from {market['name']}",
|
||||
"category": random.choice(["Electronics", "Clothing", "Forbidden Category"]),
|
||||
"image_url": "https://via.placeholder.com/150",
|
||||
"sourcing_url": ""
|
||||
})
|
||||
return products
|
||||
|
||||
def sourcing_product(image_url, sourcing_market):
|
||||
# RapidAPI를 이용한 소싱 기능 (시뮬레이션)
|
||||
app_logger.info(f"Sourcing product for image {image_url} from {sourcing_market}...")
|
||||
# 실제 구현 시 requests를 통해 API 호출
|
||||
time.sleep(0.3)
|
||||
# 더미 소싱 URL 반환
|
||||
return f"https://sourcing.example.com/{sourcing_market}/" + image_url.split("/")[-1]
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
from supabase import create_client, Client
|
||||
from supabase.lib.client_options import ClientOptions
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
class DBeManager:
|
||||
"""
|
||||
SupabaseManager는 Supabase 클라이언트를 래핑하여 로그인, 사용자 정보 조회,
|
||||
마지막 로그인 시간 업데이트 등 여러 API 호출을 수행합니다.
|
||||
"""
|
||||
def __init__(self, logger: logging.Logger):
|
||||
self.logger = logger
|
||||
self.url: str = "http://146.56.101.199:8000"
|
||||
self.key: str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE"
|
||||
|
||||
self.client: Client = create_client(self.url, self.key)
|
||||
self.access_token = None
|
||||
self.refresh_token = None
|
||||
|
||||
def update_client_with_token(self, access_token: str):
|
||||
try:
|
||||
options = ClientOptions(headers={"Authorization": f"Bearer {access_token}"})
|
||||
self.client = create_client(self.url, self.key, options=options)
|
||||
self.logger.log("Client updated with JWT token", level=logging.DEBUG)
|
||||
except Exception as ex:
|
||||
self.logger.log(f"update_client_with_token error: {ex}", level=logging.ERROR, exc_info=True)
|
||||
|
||||
def login(self, email: str, password: str) -> dict:
|
||||
"""
|
||||
Supabase Auth를 사용해 로그인합니다.
|
||||
로그인 성공 시 사용자 정보를 담은 dict를, 실패 시 error 키를 포함한 dict를 반환합니다.
|
||||
"""
|
||||
try:
|
||||
response = self.client.auth.sign_in_with_password({"email": email, "password": password})
|
||||
if response.session:
|
||||
if response.user.email_confirmed_at is None:
|
||||
self.logger.log("이메일 인증이 완료되지 않았습니다.", level=logging.WARNING)
|
||||
return {"error": "이메일 인증을 먼저 완료해 주세요."}
|
||||
self.access_token = response.session.access_token
|
||||
self.refresh_token = response.session.refresh_token
|
||||
self.update_client_with_token(self.access_token)
|
||||
user_info = {
|
||||
"id": response.user.id,
|
||||
"email": response.user.email,
|
||||
"nickname": response.user.user_metadata.get("nickname", "Unknown")
|
||||
}
|
||||
self.logger.log(f"로그인 성공: {user_info}", level=logging.DEBUG)
|
||||
return user_info
|
||||
else:
|
||||
self.logger.log("로그인 실패: 세션 없음", level=logging.WARNING)
|
||||
return {"error": "로그인 실패"}
|
||||
except Exception as e:
|
||||
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:
|
||||
"""
|
||||
auth.users 테이블(인증 스키마)에서 사용자 정보를 조회합니다.
|
||||
"""
|
||||
try:
|
||||
response = self.client.from_("users").select("email_confirmed_at").eq("id", user_id).execute()
|
||||
if response.data and len(response.data) > 0:
|
||||
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:
|
||||
"""
|
||||
public.users 테이블과 membership_levels 테이블을 조합하여
|
||||
전체 사용자 정보를 반환합니다.
|
||||
"""
|
||||
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}
|
||||
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):
|
||||
"""
|
||||
사용자의 마지막 로그인 시간을 현재 시간(UTC)으로 업데이트합니다.
|
||||
"""
|
||||
try:
|
||||
now_iso = datetime.now(timezone.utc).isoformat()
|
||||
self.client.table("users").update({"last_login": now_iso}).eq("id", user_id).execute()
|
||||
self.logger.log(f"Last login updated for user {user_id}", level=logging.INFO)
|
||||
except Exception as e:
|
||||
self.logger.log(f"update_last_login 에러: {e}", level=logging.ERROR, exc_info=True)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import pandas as pd
|
||||
import os
|
||||
from datetime import datetime
|
||||
import webbrowser
|
||||
|
||||
def export_to_excel(products):
|
||||
if not products:
|
||||
return
|
||||
# 상품을 50개씩 분할
|
||||
batches = [products[i:i+50] for i in range(0, len(products), 50)]
|
||||
export_folder = "exported_products"
|
||||
os.makedirs(export_folder, exist_ok=True)
|
||||
for idx, batch in enumerate(batches):
|
||||
df = pd.DataFrame(batch)
|
||||
filename = os.path.join(export_folder, f"products_batch_{idx+1}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx")
|
||||
df.to_excel(filename, index=False)
|
||||
# 저장 폴더 열기
|
||||
webbrowser.open(os.path.abspath(export_folder))
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
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)
|
||||
"""
|
||||
self.gui_callback = gui_callback
|
||||
self.logger = logging.getLogger(logger_name)
|
||||
self.logger.setLevel(level)
|
||||
# 파일 핸들러 설정 (RotatingFileHandler)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
def format_gui_message(self, message, level):
|
||||
"""
|
||||
레벨별 색상을 적용한 HTML 문자열 반환.
|
||||
"""
|
||||
color_map = {
|
||||
logging.DEBUG: "gray",
|
||||
logging.INFO: "black",
|
||||
logging.WARNING: "orange",
|
||||
logging.ERROR: "red",
|
||||
logging.CRITICAL: "purple"
|
||||
}
|
||||
color = color_map.get(level, "black")
|
||||
return f'<span style="color:{color};">{message}</span>'
|
||||
|
||||
def get_logger(gui_callback=None):
|
||||
return Logger(gui_callback=gui_callback)
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import flet as ft
|
||||
from flet import (
|
||||
AlertDialog, TextField, Checkbox, ElevatedButton, Text, Column, Row,
|
||||
MainAxisAlignment
|
||||
)
|
||||
import asyncio
|
||||
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.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.password_field.password = not self.show_password_checkbox.value
|
||||
self.page.update()
|
||||
|
||||
def on_login(self, e: ft.ControlEvent):
|
||||
email = self.email_field.value.strip()
|
||||
password = self.password_field.value.strip()
|
||||
if not email or not password:
|
||||
self.error_text.value = "이메일과 비밀번호를 모두 입력하세요."
|
||||
self.page.update()
|
||||
return
|
||||
# 실제 Supabase 로그인을 시도합니다.
|
||||
result = self.supabase_manager.login(email, password)
|
||||
if "error" not in result:
|
||||
self.logger.log(f"로그인 성공: {result}", level=logging.DEBUG)
|
||||
# 마지막 로그인 시간 업데이트
|
||||
self.supabase_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.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.error_text.value = "비밀번호 찾기 기능은 구현되지 않았습니다."
|
||||
self.page.update()
|
||||
|
||||
async def show(self) -> bool:
|
||||
"""
|
||||
로그인 다이얼로그를 보여주고, 로그인 성공 시 True를 반환합니다.
|
||||
(비동기적으로 result가 채워질 때까지 기다립니다.)
|
||||
"""
|
||||
while self.result is None:
|
||||
await asyncio.sleep(0.1)
|
||||
return self.result
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
def filter_forbidden_words(products):
|
||||
# 데모용 금지어 리스트
|
||||
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)
|
||||
return filtered
|
||||
|
||||
def filter_forbidden_categories(products):
|
||||
# 데모용 금지 카테고리 리스트
|
||||
forbidden_categories = ["Forbidden Category"]
|
||||
filtered = []
|
||||
for product in products:
|
||||
if product.get("category", "") not in forbidden_categories:
|
||||
filtered.append(product)
|
||||
return filtered
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
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):
|
||||
if os.path.exists(self.filename):
|
||||
try:
|
||||
with open(self.filename, "r", encoding="utf-8") as f:
|
||||
self.settings = json.load(f)
|
||||
except Exception as e:
|
||||
print("설정 불러오기 오류:", e)
|
||||
self.settings = {}
|
||||
else:
|
||||
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):
|
||||
self.settings["user"] = user_info
|
||||
self.save_settings()
|
||||
|
||||
def load_user_info(self) -> dict:
|
||||
return self.settings.get("user", {})
|
||||
|
||||
def save_gui_settings(self, gui_settings: dict):
|
||||
self.settings["gui"] = gui_settings
|
||||
self.save_settings()
|
||||
|
||||
def load_gui_settings(self) -> dict:
|
||||
return self.settings.get("gui", {})
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,25 @@
|
|||
[project]
|
||||
name = "Resell1"
|
||||
version = "1.0.0"
|
||||
description = "나도 좀 팔자"
|
||||
authors = [
|
||||
{name = "WhenRideMyCar",email = "kkebiini@gmail.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11,<4.0"
|
||||
dependencies = [
|
||||
"flet (>=0.27.6,<0.28.0)",
|
||||
"pandas (>=2.2.3,<3.0.0)",
|
||||
"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)"
|
||||
]
|
||||
|
||||
[tool.poetry]
|
||||
packages = [{include = "resell1", from = "src"}]
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
Loading…
Reference in New Issue