This commit is contained in:
parent
f766f5706b
commit
c56dcd8a29
BIN
markets.db
BIN
markets.db
Binary file not shown.
440
t2.py
440
t2.py
|
|
@ -2,22 +2,140 @@ import flet as ft
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, time
|
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
import traceback
|
import traceback
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import psutil
|
import psutil
|
||||||
import glob
|
import glob
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
# 로그 기본 설정
|
# 로깅 기본 설정
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
# --- BookmarkWorker (백그라운드 작업) ---
|
# =====================================================================
|
||||||
|
# DB 관련 기능을 담당하는 모듈 (DBHandler)
|
||||||
|
# =====================================================================
|
||||||
|
class DBHandler:
|
||||||
|
def __init__(self, db_path="markets.db"):
|
||||||
|
self.db_path = db_path
|
||||||
|
self.ensure_db()
|
||||||
|
|
||||||
|
def ensure_db(self):
|
||||||
|
if not os.path.exists(self.db_path):
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE markets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
country TEXT,
|
||||||
|
mall_grade TEXT,
|
||||||
|
mall_name TEXT,
|
||||||
|
mall_url TEXT,
|
||||||
|
extract_count INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def load_excel(self, file_path, remove_existing=False, log_callback=print):
|
||||||
|
try:
|
||||||
|
ext = os.path.splitext(file_path)[-1].lower()
|
||||||
|
if ext == ".xls":
|
||||||
|
df = pd.read_excel(file_path, sheet_name=0, engine="xlrd")
|
||||||
|
elif ext == ".xlsx":
|
||||||
|
df = pd.read_excel(file_path, sheet_name=0, engine="openpyxl")
|
||||||
|
else:
|
||||||
|
log_callback("지원되지 않는 파일 형식입니다. (.xls 또는 .xlsx)")
|
||||||
|
return
|
||||||
|
|
||||||
|
required_columns = ['country', 'mall_grade', 'mall_name', 'mall_url']
|
||||||
|
for col in required_columns:
|
||||||
|
if col not in df.columns:
|
||||||
|
df[col] = ""
|
||||||
|
df = df[required_columns]
|
||||||
|
df.drop_duplicates(subset=required_columns, inplace=True)
|
||||||
|
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
if remove_existing:
|
||||||
|
conn.execute("DROP TABLE IF EXISTS markets")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE markets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
country TEXT,
|
||||||
|
mall_grade TEXT,
|
||||||
|
mall_name TEXT,
|
||||||
|
mall_url TEXT,
|
||||||
|
extract_count INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE markets ADD COLUMN extract_count INTEGER DEFAULT 0")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO markets (country, mall_grade, mall_name, mall_url)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (row['country'], row['mall_grade'], row['mall_name'], row['mall_url']))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log_callback("DB 저장 완료")
|
||||||
|
except Exception as e:
|
||||||
|
log_callback(f"엑셀 로드 에러: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
|
def view_data(self):
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
df = pd.read_sql_query("SELECT * FROM markets", conn)
|
||||||
|
conn.close()
|
||||||
|
return df
|
||||||
|
|
||||||
|
def reset_extract_count(self, log_callback=print):
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.execute("UPDATE markets SET extract_count = 0")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log_callback("모든 추출 횟수가 초기화되었습니다.")
|
||||||
|
|
||||||
|
def extract_bookmarks(self, country, grade, count, extract_based, max_extract, log_callback=print):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
query = "SELECT id, mall_name AS name, mall_url AS url FROM markets WHERE 1=1"
|
||||||
|
if country != "랜덤":
|
||||||
|
query += f" AND country = '{country}'"
|
||||||
|
if grade != "랜덤":
|
||||||
|
query += f" AND mall_grade = '{grade}'"
|
||||||
|
if extract_based:
|
||||||
|
query += " AND extract_count < ? ORDER BY extract_count ASC, RANDOM()"
|
||||||
|
query += f" LIMIT {count}"
|
||||||
|
df = pd.read_sql_query(query, conn, params=(max_extract,))
|
||||||
|
else:
|
||||||
|
query += " ORDER BY RANDOM()"
|
||||||
|
query += f" LIMIT {count}"
|
||||||
|
df = pd.read_sql_query(query, conn)
|
||||||
|
if df.empty:
|
||||||
|
log_callback("추출 가능한 데이터가 없습니다.")
|
||||||
|
conn.close()
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 추출 횟수 업데이트
|
||||||
|
for record in df.to_dict("records"):
|
||||||
|
conn.execute("UPDATE markets SET extract_count = extract_count + 1 WHERE id = ?", (record["id"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log_callback(f"{len(df)}개의 북마크를 추출했습니다.")
|
||||||
|
return df.to_dict("records")
|
||||||
|
except Exception as e:
|
||||||
|
log_callback(f"DB 쿼리 실행 오류: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 북마크 추가 작업을 백그라운드로 처리하는 모듈 (BookmarkWorker)
|
||||||
|
# =====================================================================
|
||||||
class BookmarkWorker(threading.Thread):
|
class BookmarkWorker(threading.Thread):
|
||||||
def __init__(self, bookmarks, folder_name, bookmarks_path, browser_path, selected_browser, remove_existing, progress_callback, log_callback, completed_callback):
|
def __init__(self, bookmarks, folder_name, bookmarks_path, browser_path, selected_browser, remove_existing,
|
||||||
|
progress_callback, log_callback, completed_callback):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.bookmarks = bookmarks
|
self.bookmarks = bookmarks
|
||||||
self.folder_name = folder_name
|
self.folder_name = folder_name
|
||||||
|
|
@ -31,7 +149,6 @@ class BookmarkWorker(threading.Thread):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
try:
|
try:
|
||||||
# JSON 파일 읽기
|
|
||||||
if not os.path.exists(self.bookmarks_path):
|
if not os.path.exists(self.bookmarks_path):
|
||||||
self.log_callback(f"즐겨찾기 JSON 파일을 찾을 수 없습니다: {self.bookmarks_path}")
|
self.log_callback(f"즐겨찾기 JSON 파일을 찾을 수 없습니다: {self.bookmarks_path}")
|
||||||
return
|
return
|
||||||
|
|
@ -45,91 +162,75 @@ class BookmarkWorker(threading.Thread):
|
||||||
else:
|
else:
|
||||||
bookmarks_data = json.loads(file_content)
|
bookmarks_data = json.loads(file_content)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
self.log_callback(f"JSON 파일 파싱 중 오류 발생: {str(e)}")
|
self.log_callback(f"JSON 파싱 오류: {e}")
|
||||||
bookmarks_data = {"roots": {"bookmark_bar": {"children": []}}}
|
bookmarks_data = {"roots": {"bookmark_bar": {"children": []}}}
|
||||||
self.log_callback("JSON 파일을 기본값으로 초기화합니다.")
|
|
||||||
|
|
||||||
# 기존 북마크 제거
|
|
||||||
if self.remove_existing:
|
if self.remove_existing:
|
||||||
bookmarks_data["roots"]["bookmark_bar"] = self.remove_existing_bookmarks(
|
bookmarks_data["roots"]["bookmark_bar"] = self.remove_existing_bookmarks(
|
||||||
bookmarks_data["roots"]["bookmark_bar"]
|
bookmarks_data["roots"]["bookmark_bar"]
|
||||||
)
|
)
|
||||||
|
|
||||||
total_bookmarks = len(self.bookmarks)
|
total = len(self.bookmarks)
|
||||||
bookmark_bar = bookmarks_data["roots"]["bookmark_bar"]
|
bookmark_bar = bookmarks_data["roots"]["bookmark_bar"]
|
||||||
|
|
||||||
# 상위 폴더 생성 (현재 날짜/시간 포함)
|
|
||||||
current_time = datetime.now().strftime("%m-%d-%H-%M-%S")
|
current_time = datetime.now().strftime("%m-%d-%H-%M-%S")
|
||||||
parent_folder_name = f"거상북마크-{current_time}"
|
parent_folder_name = f"거상북마크-{current_time}"
|
||||||
parent_folder = {
|
parent_folder = {"type": "folder", "name": parent_folder_name, "children": []}
|
||||||
"type": "folder",
|
|
||||||
"name": parent_folder_name,
|
|
||||||
"children": []
|
|
||||||
}
|
|
||||||
|
|
||||||
# 하위 폴더에 일정 개수씩 북마크 추가 (chunk 단위)
|
|
||||||
chunk_size = 100
|
chunk_size = 100
|
||||||
for idx, chunk_start in enumerate(range(0, total_bookmarks, chunk_size), start=1):
|
for idx, start in enumerate(range(0, total, chunk_size), start=1):
|
||||||
folder_name = f"거상북마크-{self.folder_name}-{idx}"
|
|
||||||
sub_folder = {
|
sub_folder = {
|
||||||
"type": "folder",
|
"type": "folder",
|
||||||
"name": folder_name,
|
"name": f"거상북마크-{self.folder_name}-{idx}",
|
||||||
"children": []
|
"children": []
|
||||||
}
|
}
|
||||||
for bookmark in self.bookmarks[chunk_start:chunk_start + chunk_size]:
|
for bm in self.bookmarks[start:start+chunk_size]:
|
||||||
sub_folder["children"].append({
|
sub_folder["children"].append({
|
||||||
"type": "url",
|
"type": "url",
|
||||||
"name": bookmark['name'],
|
"name": bm["name"],
|
||||||
"url": bookmark['url']
|
"url": bm["url"]
|
||||||
})
|
})
|
||||||
parent_folder["children"].append(sub_folder)
|
parent_folder["children"].append(sub_folder)
|
||||||
progress = int((chunk_start + len(self.bookmarks[chunk_start:chunk_start + chunk_size])) / total_bookmarks * 100)
|
progress = int((start + min(chunk_size, total - start)) / total * 100)
|
||||||
self.progress_callback(progress)
|
self.progress_callback(progress)
|
||||||
|
|
||||||
bookmark_bar["children"].append(parent_folder)
|
bookmark_bar["children"].append(parent_folder)
|
||||||
with open(self.bookmarks_path, "w", encoding="utf-8") as file:
|
with open(self.bookmarks_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(bookmarks_data, file, indent=4, ensure_ascii=False)
|
json.dump(bookmarks_data, f, indent=4, ensure_ascii=False)
|
||||||
|
self.log_callback("즐겨찾기 추가 완료!")
|
||||||
|
|
||||||
self.log_callback("즐겨찾기 추가 작업이 완료되었습니다!")
|
self.run_browser_with_profile(self.browser_path, self.bookmarks_path, self.selected_browser)
|
||||||
self.run_browser_with_profile(self.browser_path, self.bookmarks_path, browser_name=self.selected_browser)
|
|
||||||
self.run_and_focus_copyman()
|
self.run_and_focus_copyman()
|
||||||
self.completed_callback()
|
self.completed_callback()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_callback(f"오류 발생: {str(e)}\n{traceback.format_exc()}")
|
self.log_callback(f"작업 오류: {e}\n{traceback.format_exc()}")
|
||||||
self.progress_callback(0)
|
self.progress_callback(0)
|
||||||
|
|
||||||
def run_browser_with_profile(self, browser_path, bookmarks_path, browser_name="브라우저"):
|
def run_browser_with_profile(self, browser_path, bookmarks_path, browser_name):
|
||||||
# 웨일 브라우저의 경우 기존 프로세스 종료 시도
|
|
||||||
if "웨일" in browser_name:
|
if "웨일" in browser_name:
|
||||||
for proc in psutil.process_iter(attrs=["pid", "name"]):
|
for proc in psutil.process_iter(attrs=["pid", "name"]):
|
||||||
if "whale" in proc.info["name"].lower():
|
if "whale" in proc.info["name"].lower():
|
||||||
try:
|
try:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
proc.wait(timeout=5)
|
proc.wait(timeout=5)
|
||||||
self.log_callback("웨일 브라우저의 기존 프로세스를 종료했습니다.")
|
self.log_callback("웨일 프로세스 종료됨.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_callback(f"웨일 브라우저 프로세스 종료 중 오류 발생: {str(e)}")
|
self.log_callback(f"웨일 종료 오류: {e}")
|
||||||
if os.path.exists(browser_path):
|
if os.path.exists(browser_path):
|
||||||
profile_directory = os.path.basename(os.path.dirname(bookmarks_path))
|
profile_dir = os.path.basename(os.path.dirname(bookmarks_path))
|
||||||
if "크롬" in browser_name.lower():
|
if "크롬" in browser_name.lower():
|
||||||
bookmark_page = "chrome://bookmarks/"
|
page_url = "chrome://bookmarks/"
|
||||||
elif "웨일" in browser_name.lower():
|
elif "웨일" in browser_name.lower():
|
||||||
bookmark_page = "whale://bookmarks/"
|
page_url = "whale://bookmarks/"
|
||||||
else:
|
else:
|
||||||
bookmark_page = "about:blank"
|
page_url = "about:blank"
|
||||||
if profile_directory:
|
if profile_dir:
|
||||||
subprocess.Popen([
|
subprocess.Popen([browser_path, f"--profile-directory={profile_dir}", page_url])
|
||||||
browser_path,
|
self.log_callback(f"{browser_name} 프로필 {profile_dir}로 북마크 열기")
|
||||||
f"--profile-directory={profile_directory}",
|
|
||||||
bookmark_page
|
|
||||||
])
|
|
||||||
self.log_callback(f"{browser_name} 북마크 관리 페이지를 '{profile_directory}' 프로필로 열었습니다.")
|
|
||||||
else:
|
else:
|
||||||
subprocess.Popen([browser_path, bookmark_page])
|
subprocess.Popen([browser_path, page_url])
|
||||||
self.log_callback(f"{browser_name} 북마크 관리 페이지를 기본 프로필로 열었습니다.")
|
self.log_callback(f"{browser_name} 기본 프로필로 북마크 열기")
|
||||||
else:
|
else:
|
||||||
self.log_callback(f"{browser_name} 실행 파일을 찾을 수 없습니다: {browser_path}")
|
self.log_callback(f"{browser_name} 경로가 올바르지 않습니다: {browser_path}")
|
||||||
|
|
||||||
def run_and_focus_copyman(self):
|
def run_and_focus_copyman(self):
|
||||||
program_name = "@카피맨.exe"
|
program_name = "@카피맨.exe"
|
||||||
|
|
@ -140,16 +241,16 @@ class BookmarkWorker(threading.Thread):
|
||||||
if pid:
|
if pid:
|
||||||
self.log_callback(f"{program_name} 실행 중 (PID: {pid})")
|
self.log_callback(f"{program_name} 실행 중 (PID: {pid})")
|
||||||
if not self.focus_window_by_title(window_title_start):
|
if not self.focus_window_by_title(window_title_start):
|
||||||
self.log_callback(f"'{window_title_start}'로 시작하는 창을 찾을 수 없습니다.")
|
self.log_callback("카피맨 창을 찾지 못함.")
|
||||||
else:
|
else:
|
||||||
self.log_callback(f"{program_name} 실행 중이 아님. 실행 시도 중...")
|
self.log_callback("카피맨 실행 안됨. 실행 시도...")
|
||||||
shortcut_path = self.find_shortcut_in_start_menu(shortcut_name)
|
shortcut = self.find_shortcut_in_start_menu(shortcut_name)
|
||||||
if shortcut_path:
|
if shortcut:
|
||||||
self.run_program(shortcut_path)
|
self.run_program(shortcut)
|
||||||
else:
|
else:
|
||||||
self.log_callback(f"'{shortcut_name}' 바로가기를 찾을 수 없습니다.")
|
self.log_callback("카피맨 바로가기 없음.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_callback(f"카피맨 실행 중 오류 발생: {str(e)}\n{traceback.format_exc()}")
|
self.log_callback(f"카피맨 실행 오류: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
def is_program_running(self, process_name):
|
def is_program_running(self, process_name):
|
||||||
for proc in psutil.process_iter(attrs=["pid", "name"]):
|
for proc in psutil.process_iter(attrs=["pid", "name"]):
|
||||||
|
|
@ -164,23 +265,23 @@ class BookmarkWorker(threading.Thread):
|
||||||
if window.title and window.title.startswith(title_start):
|
if window.title and window.title.startswith(title_start):
|
||||||
try:
|
try:
|
||||||
window.activate()
|
window.activate()
|
||||||
self.log_callback(f"프로그램 창으로 전환: {window.title}")
|
self.log_callback(f"창 활성화: {window.title}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_callback(f"창 활성화 실패: {e}")
|
self.log_callback(f"창 활성화 실패: {e}")
|
||||||
return False
|
return False
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_callback(f"윈도우 포커스 전환 오류: {e}")
|
self.log_callback(f"포커스 전환 오류: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def find_shortcut_in_start_menu(self, shortcut_name):
|
def find_shortcut_in_start_menu(self, shortcut_name):
|
||||||
user_start_menu = os.path.expandvars(r"%APPDATA%\Microsoft\Windows\Start Menu\Programs")
|
user_menu = os.path.expandvars(r"%APPDATA%\Microsoft\Windows\Start Menu\Programs")
|
||||||
all_users_start_menu = os.path.expandvars(r"%ProgramData%\Microsoft\Windows\Start Menu\Programs")
|
all_users_menu = os.path.expandvars(r"%ProgramData%\Microsoft\Windows\Start Menu\Programs")
|
||||||
for start_menu_path in [user_start_menu, all_users_start_menu]:
|
for path in [user_menu, all_users_menu]:
|
||||||
shortcut_path = glob.glob(os.path.join(start_menu_path, f"**\\{shortcut_name}.lnk"), recursive=True)
|
shortcut = glob.glob(os.path.join(path, f"**\\{shortcut_name}.lnk"), recursive=True)
|
||||||
if shortcut_path:
|
if shortcut:
|
||||||
return shortcut_path[0]
|
return shortcut[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def run_program(self, shortcut_path):
|
def run_program(self, shortcut_path):
|
||||||
|
|
@ -188,7 +289,7 @@ class BookmarkWorker(threading.Thread):
|
||||||
subprocess.Popen([shortcut_path], shell=True)
|
subprocess.Popen([shortcut_path], shell=True)
|
||||||
self.log_callback(f"프로그램 실행: {shortcut_path}")
|
self.log_callback(f"프로그램 실행: {shortcut_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_callback(f"프로그램 실행 실패: {e}")
|
self.log_callback(f"실행 오류: {e}")
|
||||||
|
|
||||||
def remove_existing_bookmarks(self, node):
|
def remove_existing_bookmarks(self, node):
|
||||||
if not isinstance(node, dict):
|
if not isinstance(node, dict):
|
||||||
|
|
@ -197,46 +298,41 @@ class BookmarkWorker(threading.Thread):
|
||||||
self.log_callback(f"제거된 폴더: {node.get('name')}")
|
self.log_callback(f"제거된 폴더: {node.get('name')}")
|
||||||
return None
|
return None
|
||||||
if "children" in node:
|
if "children" in node:
|
||||||
node["children"] = [
|
node["children"] = [self.remove_existing_bookmarks(child)
|
||||||
self.remove_existing_bookmarks(child)
|
for child in node["children"]
|
||||||
for child in node["children"]
|
if self.remove_existing_bookmarks(child) is not None]
|
||||||
if self.remove_existing_bookmarks(child) is not None
|
|
||||||
]
|
|
||||||
return node
|
return node
|
||||||
|
|
||||||
# --- Flet 기반 메인 앱 ---
|
# =====================================================================
|
||||||
|
# Flet UI와 백엔드 모듈을 통합한 메인 앱
|
||||||
|
# =====================================================================
|
||||||
def main(page: ft.Page):
|
def main(page: ft.Page):
|
||||||
page.title = "즐겨찾기 추가 앱 (Flet 버전)"
|
page.title = "모듈화된 백엔드 & Flet 앱"
|
||||||
page.horizontal_alignment = "center"
|
page.horizontal_alignment = "center"
|
||||||
page.vertical_alignment = "start"
|
page.vertical_alignment = "start"
|
||||||
page.padding = 20
|
page.padding = 20
|
||||||
|
|
||||||
# 전역 상태 변수 (필요에 따라 수정)
|
# 기본 비밀번호 및 DB 핸들러 생성
|
||||||
db_path = "markets.db"
|
|
||||||
bookmarks = [] # DB에서 추출한 북마크 목록
|
|
||||||
chrome_path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
|
||||||
whale_path = "C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe"
|
|
||||||
# 브라우저별 북마크 파일 경로 (사용자 환경에 맞게 설정 필요)
|
|
||||||
chrome_bookmarks_path = "C:\\Users\\User\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\Bookmarks"
|
|
||||||
whale_bookmarks_path = "C:\\Users\\User\\AppData\\Local\\Naver\\Naver Whale\\User Data\\Default\\Bookmarks"
|
|
||||||
stored_password = "365"
|
stored_password = "365"
|
||||||
user_password = None # 입력받은 비밀번호 저장
|
user_password = None
|
||||||
|
db_handler = DBHandler()
|
||||||
|
bookmarks_global = [] # 추출된 북마크 저장용
|
||||||
|
|
||||||
# --- 로그 업데이트 함수 ---
|
# 로그와 진행률 UI
|
||||||
log_display = ft.Text(value="", size=12)
|
log_text = ft.Text(value="", size=12)
|
||||||
def log(msg: str):
|
progress_bar = ft.ProgressBar(width=300, value=0)
|
||||||
log_display.value += msg + "\n"
|
|
||||||
|
def log(msg):
|
||||||
|
nonlocal log_text
|
||||||
|
log_text.value += msg + "\n"
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
# --- 진행률 업데이트 함수 ---
|
def update_progress(val):
|
||||||
progress_bar = ft.ProgressBar(width=300, value=0)
|
|
||||||
def update_progress(val: int):
|
|
||||||
progress_bar.value = val / 100
|
progress_bar.value = val / 100
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
# --- 작업 완료 후 호출 함수 ---
|
|
||||||
def task_completed():
|
def task_completed():
|
||||||
log("작업이 완료되었습니다!")
|
log("작업 완료!")
|
||||||
dlg = ft.AlertDialog(
|
dlg = ft.AlertDialog(
|
||||||
title=ft.Text("완료"),
|
title=ft.Text("완료"),
|
||||||
content=ft.Text("즐겨찾기 추가 작업이 완료되었습니다."),
|
content=ft.Text("즐겨찾기 추가 작업이 완료되었습니다."),
|
||||||
|
|
@ -251,29 +347,29 @@ def main(page: ft.Page):
|
||||||
page.dialog.open = False
|
page.dialog.open = False
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
# --- 비밀번호 입력 다이얼로그 (모달) ---
|
# 비밀번호 입력 다이얼로그
|
||||||
def show_password_dialog():
|
def show_password_dialog():
|
||||||
def on_password_submit(e):
|
def on_password_submit(e):
|
||||||
nonlocal user_password
|
nonlocal user_password
|
||||||
user_password = password_field.value
|
user_password = pwd_field.value
|
||||||
if user_password != stored_password:
|
if user_password != stored_password:
|
||||||
result_dialog = ft.AlertDialog(
|
result = ft.AlertDialog(
|
||||||
title=ft.Text("비밀번호 오류"),
|
title=ft.Text("비밀번호 오류"),
|
||||||
content=ft.Text("비밀번호가 일치하지 않습니다. 프로그램을 종료합니다."),
|
content=ft.Text("비밀번호가 일치하지 않습니다. 프로그램을 종료합니다."),
|
||||||
actions=[ft.TextButton("닫기", on_click=lambda e: page.window_close())],
|
actions=[ft.TextButton("닫기", on_click=lambda e: page.window_close())],
|
||||||
modal=True
|
modal=True
|
||||||
)
|
)
|
||||||
page.dialog = result_dialog
|
page.dialog = result
|
||||||
result_dialog.open = True
|
result.open = True
|
||||||
page.update()
|
page.update()
|
||||||
else:
|
else:
|
||||||
pwd_dlg.open = False
|
pwd_dlg.open = False
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
password_field = ft.TextField(label="비밀번호", password=True)
|
pwd_field = ft.TextField(label="비밀번호", password=True)
|
||||||
pwd_dlg = ft.AlertDialog(
|
pwd_dlg = ft.AlertDialog(
|
||||||
title=ft.Text("비밀번호 입력"),
|
title=ft.Text("비밀번호 입력"),
|
||||||
content=ft.Column([password_field]),
|
content=ft.Column([pwd_field]),
|
||||||
actions=[ft.TextButton("확인", on_click=on_password_submit)],
|
actions=[ft.TextButton("확인", on_click=on_password_submit)],
|
||||||
modal=True
|
modal=True
|
||||||
)
|
)
|
||||||
|
|
@ -283,7 +379,9 @@ def main(page: ft.Page):
|
||||||
|
|
||||||
show_password_dialog()
|
show_password_dialog()
|
||||||
|
|
||||||
# --- UI 컨트롤 ---
|
# =================================================================
|
||||||
|
# UI 컨트롤 구성
|
||||||
|
# =================================================================
|
||||||
country_dropdown = ft.Dropdown(
|
country_dropdown = ft.Dropdown(
|
||||||
label="국가",
|
label="국가",
|
||||||
options=[
|
options=[
|
||||||
|
|
@ -323,20 +421,79 @@ def main(page: ft.Page):
|
||||||
value="크롬"
|
value="크롬"
|
||||||
)
|
)
|
||||||
|
|
||||||
db_input_button = ft.ElevatedButton("DB 입력", on_click=lambda e: log("DB 입력 버튼 클릭 - 엑셀 파일 선택 기능 구현 필요"))
|
# 파일 선택: 엑셀 파일 입력
|
||||||
view_data_button = ft.ElevatedButton("데이터 보기", on_click=lambda e: log("데이터 보기 버튼 클릭 - DB 테이블 표시 기능 구현 필요"))
|
file_picker_excel = ft.FilePicker(on_result=lambda e: on_excel_selected(e))
|
||||||
chrome_path_button = ft.ElevatedButton("크롬 경로 설정", on_click=lambda e: log("크롬 경로 설정 버튼 클릭 - 파일 선택 구현 필요"))
|
page.overlay.append(file_picker_excel)
|
||||||
whale_path_button = ft.ElevatedButton("웨일 경로 설정", on_click=lambda e: log("웨일 경로 설정 버튼 클릭 - 파일 선택 구현 필요"))
|
|
||||||
run_button = ft.ElevatedButton("실행")
|
|
||||||
|
|
||||||
# --- 실행 버튼 이벤트 (run_task) ---
|
def on_excel_selected(e: ft.FilePickerResultEvent):
|
||||||
|
if e.files is not None and len(e.files) > 0:
|
||||||
|
file_path = e.files[0].path
|
||||||
|
db_handler.load_excel(file_path, remove_existing_checkbox.value, log)
|
||||||
|
else:
|
||||||
|
log("엑셀 파일 선택 취소됨.")
|
||||||
|
|
||||||
|
db_input_button = ft.ElevatedButton("DB 입력", on_click=lambda e: file_picker_excel.pick_files(
|
||||||
|
allow_multiple=False, file_type=ft.FilePickerFileType.CUSTOM, allowed_extensions=["xlsx", "xls"]
|
||||||
|
))
|
||||||
|
|
||||||
|
view_data_button = ft.ElevatedButton("데이터 보기", on_click=lambda e: view_data_action())
|
||||||
|
def view_data_action():
|
||||||
|
df = db_handler.view_data()
|
||||||
|
if df.empty:
|
||||||
|
log("DB에 데이터가 없습니다.")
|
||||||
|
return
|
||||||
|
dlg = ft.AlertDialog(
|
||||||
|
title=ft.Text("DB 데이터"),
|
||||||
|
content=ft.Text(str(df)),
|
||||||
|
actions=[ft.TextButton("닫기", on_click=lambda e: close_dialog())],
|
||||||
|
modal=True
|
||||||
|
)
|
||||||
|
page.dialog = dlg
|
||||||
|
dlg.open = True
|
||||||
|
page.update()
|
||||||
|
|
||||||
|
reset_extract_button = ft.ElevatedButton("추출 횟수 초기화", on_click=lambda e: db_handler.reset_extract_count(log))
|
||||||
|
|
||||||
|
# 파일 선택: 브라우저 실행 파일 경로 설정 (Flet FilePicker 활용)
|
||||||
|
file_picker_browser = ft.FilePicker(on_result=lambda e: on_browser_selected(e))
|
||||||
|
page.overlay.append(file_picker_browser)
|
||||||
|
browser_path_field = ft.TextField(label="브라우저 경로", value="")
|
||||||
|
|
||||||
|
# 전역 변수 업데이트: 기본 경로 설정 (사용자 환경에 맞게 수정)
|
||||||
|
chrome_path = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
|
||||||
|
chrome_bookmarks_path = os.path.join(os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\User Data\Default"), "Bookmarks")
|
||||||
|
whale_path = "C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe"
|
||||||
|
whale_bookmarks_path = os.path.join(os.path.expandvars(r"%LOCALAPPDATA%\Naver\Naver Whale\User Data\Default"), "Bookmarks")
|
||||||
|
|
||||||
|
def on_browser_selected(e: ft.FilePickerResultEvent):
|
||||||
|
nonlocal chrome_path, whale_path
|
||||||
|
if e.files and len(e.files) > 0:
|
||||||
|
file_path = e.files[0].path
|
||||||
|
browser_path_field.value = file_path
|
||||||
|
if browser_dropdown.value == "크롬":
|
||||||
|
chrome_path = file_path
|
||||||
|
else:
|
||||||
|
whale_path = file_path
|
||||||
|
log(f"브라우저 경로 설정: {file_path}")
|
||||||
|
page.update()
|
||||||
|
else:
|
||||||
|
log("브라우저 파일 선택 취소됨.")
|
||||||
|
|
||||||
|
chrome_path_button = ft.ElevatedButton("크롬 경로 설정", on_click=lambda e: file_picker_browser.pick_files(
|
||||||
|
allow_multiple=False, file_type=ft.FilePickerFileType.CUSTOM, allowed_extensions=["exe"]
|
||||||
|
))
|
||||||
|
whale_path_button = ft.ElevatedButton("웨일 경로 설정", on_click=lambda e: file_picker_browser.pick_files(
|
||||||
|
allow_multiple=False, file_type=ft.FilePickerFileType.CUSTOM, allowed_extensions=["exe"]
|
||||||
|
))
|
||||||
|
|
||||||
|
run_button = ft.ElevatedButton("실행")
|
||||||
def run_task(e):
|
def run_task(e):
|
||||||
country = country_dropdown.value
|
country = country_dropdown.value
|
||||||
grade = grade_dropdown.value
|
grade = grade_dropdown.value
|
||||||
try:
|
try:
|
||||||
count = int(count_field.value)
|
cnt = int(count_field.value)
|
||||||
except:
|
except:
|
||||||
count = 1000
|
cnt = 1000
|
||||||
remove_existing = remove_existing_checkbox.value
|
remove_existing = remove_existing_checkbox.value
|
||||||
extract_based = extract_based_checkbox.value
|
extract_based = extract_based_checkbox.value
|
||||||
try:
|
try:
|
||||||
|
|
@ -344,82 +501,41 @@ def main(page: ft.Page):
|
||||||
except:
|
except:
|
||||||
max_extract = 1
|
max_extract = 1
|
||||||
|
|
||||||
# DB에서 조건에 맞는 데이터 추출 (간략화된 예제)
|
bookmarks_extracted = db_handler.extract_bookmarks(country, grade, cnt, extract_based, max_extract, log)
|
||||||
try:
|
if bookmarks_extracted is None:
|
||||||
conn = sqlite3.connect(db_path)
|
|
||||||
query = "SELECT id, mall_name AS name, mall_url AS url FROM markets WHERE 1=1"
|
|
||||||
if country != "랜덤":
|
|
||||||
query += f" AND country = '{country}'"
|
|
||||||
if grade != "랜덤":
|
|
||||||
query += f" AND mall_grade = '{grade}'"
|
|
||||||
if extract_based:
|
|
||||||
query += " AND extract_count < ? ORDER BY extract_count ASC, RANDOM()"
|
|
||||||
query += f" LIMIT {count}"
|
|
||||||
df = pd.read_sql_query(query, conn, params=(max_extract,))
|
|
||||||
else:
|
|
||||||
query += " ORDER BY RANDOM()"
|
|
||||||
query += f" LIMIT {count}"
|
|
||||||
df = pd.read_sql_query(query, conn)
|
|
||||||
conn.close()
|
|
||||||
if df.empty:
|
|
||||||
log("추출 가능한 데이터가 없습니다.")
|
|
||||||
return
|
|
||||||
nonlocal bookmarks
|
|
||||||
bookmarks = df.to_dict("records")
|
|
||||||
log(f"{len(bookmarks)}개의 북마크를 추출했습니다.")
|
|
||||||
except Exception as ex:
|
|
||||||
log(f"DB 쿼리 실행 중 오류 발생: {str(ex)}")
|
|
||||||
return
|
return
|
||||||
|
nonlocal bookmarks_global
|
||||||
|
bookmarks_global = bookmarks_extracted
|
||||||
folder_name = f"거상북마크-{grade}"
|
folder_name = f"거상북마크-{grade}"
|
||||||
if browser_dropdown.value == "크롬":
|
if browser_dropdown.value == "크롬":
|
||||||
selected_bookmarks_path = chrome_bookmarks_path
|
selected_bookmarks_path = chrome_bookmarks_path
|
||||||
selected_browser_path = chrome_path
|
selected_browser_path = chrome_path
|
||||||
selected_browser = "크롬"
|
selected_browser = "크롬"
|
||||||
log("크롬 브라우저가 선택되었습니다.")
|
log("크롬 브라우저 선택됨.")
|
||||||
else:
|
else:
|
||||||
selected_bookmarks_path = whale_bookmarks_path
|
selected_bookmarks_path = whale_bookmarks_path
|
||||||
selected_browser_path = whale_path
|
selected_browser_path = whale_path
|
||||||
selected_browser = "웨일"
|
selected_browser = "웨일"
|
||||||
log("웨일 브라우저가 선택되었습니다.")
|
log("웨일 브라우저 선택됨.")
|
||||||
|
|
||||||
if not os.path.exists(selected_browser_path):
|
if not os.path.exists(selected_browser_path):
|
||||||
log("선택된 브라우저 실행 파일 경로가 유효하지 않습니다.")
|
log("브라우저 실행 파일 경로가 유효하지 않습니다.")
|
||||||
return
|
return
|
||||||
if not os.path.exists(selected_bookmarks_path):
|
if not os.path.exists(selected_bookmarks_path):
|
||||||
log("선택된 브라우저의 북마크 경로가 유효하지 않습니다.")
|
log("브라우저 북마크 경로가 유효하지 않습니다.")
|
||||||
return
|
return
|
||||||
|
worker = BookmarkWorker(bookmarks_global, folder_name, selected_bookmarks_path,
|
||||||
worker = BookmarkWorker(
|
selected_browser_path, selected_browser, remove_existing,
|
||||||
bookmarks, folder_name, selected_bookmarks_path,
|
update_progress, log, task_completed)
|
||||||
selected_browser_path, selected_browser, remove_existing,
|
|
||||||
update_progress, log, task_completed
|
|
||||||
)
|
|
||||||
worker.start()
|
worker.start()
|
||||||
|
|
||||||
run_button.on_click = run_task
|
run_button.on_click = run_task
|
||||||
|
|
||||||
# --- 레이아웃 구성 ---
|
controls_row = ft.Row(controls=[country_dropdown, grade_dropdown, count_field, remove_existing_checkbox])
|
||||||
controls_row = ft.Row(controls=[
|
extract_row = ft.Row(controls=[extract_based_checkbox, max_extract_field, reset_extract_button])
|
||||||
country_dropdown,
|
browser_row = ft.Row(controls=[browser_dropdown, chrome_path_button, whale_path_button, browser_path_field])
|
||||||
grade_dropdown,
|
|
||||||
count_field,
|
|
||||||
remove_existing_checkbox
|
|
||||||
])
|
|
||||||
extract_row = ft.Row(controls=[extract_based_checkbox, max_extract_field])
|
|
||||||
browser_row = ft.Row(controls=[browser_dropdown, chrome_path_button, whale_path_button])
|
|
||||||
action_row = ft.Row(controls=[db_input_button, view_data_button, run_button])
|
action_row = ft.Row(controls=[db_input_button, view_data_button, run_button])
|
||||||
|
|
||||||
page.add(
|
page.add(controls_row, extract_row, browser_row, action_row, progress_bar, ft.Text("로그:"), log_text)
|
||||||
controls_row,
|
|
||||||
extract_row,
|
|
||||||
browser_row,
|
|
||||||
action_row,
|
|
||||||
progress_bar,
|
|
||||||
ft.Text("로그:", size=14),
|
|
||||||
log_display
|
|
||||||
)
|
|
||||||
|
|
||||||
page.update()
|
page.update()
|
||||||
|
|
||||||
ft.app(target=main)
|
ft.app(target=main)
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,20 @@
|
||||||
|
# chrome_sync.py
|
||||||
|
# 크롬 동기화 기능을 위한 플레이스홀더 모듈입니다.
|
||||||
|
# 실제 Chrome 계정 동기화 API를 사용하려면 별도의 구현이 필요합니다.
|
||||||
|
|
||||||
|
def get_chrome_bookmarks():
|
||||||
|
"""
|
||||||
|
크롬 동기화된 북마크를 가져오는 플레이스홀더 함수.
|
||||||
|
각 북마크는 이름, URL, 폴더 경로 정보를 포함합니다.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
{"name": "Google", "url": "https://www.google.com", "folder": "검색엔진"},
|
||||||
|
{"name": "YouTube", "url": "https://www.youtube.com", "folder": "동영상"},
|
||||||
|
{"name": "GitHub", "url": "https://www.github.com", "folder": "개발"}
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_chrome_extensions():
|
||||||
|
"""
|
||||||
|
크롬 동기화된 확장 프로그램 목록을 가져오는 플레이스홀더 함수.
|
||||||
|
"""
|
||||||
|
return ["Adblock", "Grammarly", "LastPass"]
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
import eel
|
||||||
|
import sqlite3
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
import traceback
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import glob
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime, time
|
||||||
|
|
||||||
|
# 로깅 기본 설정
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# DB 관련 기능을 담당하는 모듈 (DBHandler)
|
||||||
|
# =====================================================================
|
||||||
|
class DBHandler:
|
||||||
|
def __init__(self, db_path="markets.db"):
|
||||||
|
self.db_path = db_path
|
||||||
|
self.ensure_db()
|
||||||
|
|
||||||
|
def ensure_db(self):
|
||||||
|
if not os.path.exists(self.db_path):
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE markets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
country TEXT,
|
||||||
|
mall_grade TEXT,
|
||||||
|
mall_name TEXT,
|
||||||
|
mall_url TEXT,
|
||||||
|
extract_count INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
def load_excel(self, file_path, remove_existing=False, log_callback=print):
|
||||||
|
try:
|
||||||
|
ext = os.path.splitext(file_path)[-1].lower()
|
||||||
|
if ext == ".xls":
|
||||||
|
df = pd.read_excel(file_path, sheet_name=0, engine="xlrd")
|
||||||
|
elif ext == ".xlsx":
|
||||||
|
df = pd.read_excel(file_path, sheet_name=0, engine="openpyxl")
|
||||||
|
else:
|
||||||
|
log_callback("지원되지 않는 파일 형식입니다. (.xls 또는 .xlsx)")
|
||||||
|
return "지원되지 않는 파일 형식입니다. (.xls 또는 .xlsx)"
|
||||||
|
required_columns = ['country', 'mall_grade', 'mall_name', 'mall_url']
|
||||||
|
for col in required_columns:
|
||||||
|
if col not in df.columns:
|
||||||
|
df[col] = ""
|
||||||
|
df = df[required_columns]
|
||||||
|
df.drop_duplicates(subset=required_columns, inplace=True)
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
if remove_existing:
|
||||||
|
conn.execute("DROP TABLE IF EXISTS markets")
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE markets (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
country TEXT,
|
||||||
|
mall_grade TEXT,
|
||||||
|
mall_name TEXT,
|
||||||
|
mall_url TEXT,
|
||||||
|
extract_count INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
try:
|
||||||
|
conn.execute("ALTER TABLE markets ADD COLUMN extract_count INTEGER DEFAULT 0")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
pass
|
||||||
|
for _, row in df.iterrows():
|
||||||
|
conn.execute("""
|
||||||
|
INSERT INTO markets (country, mall_grade, mall_name, mall_url)
|
||||||
|
VALUES (?, ?, ?, ?)
|
||||||
|
""", (row['country'], row['mall_grade'], row['mall_name'], row['mall_url']))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log_callback("DB 저장 완료")
|
||||||
|
return "DB 저장 완료"
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"엑셀 로드 에러: {e}\n{traceback.format_exc()}"
|
||||||
|
log_callback(msg)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def view_data(self):
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
df = pd.read_sql_query("SELECT * FROM markets", conn)
|
||||||
|
conn.close()
|
||||||
|
# HTML 테이블 형식으로 반환
|
||||||
|
return df.to_html()
|
||||||
|
|
||||||
|
def reset_extract_count(self, log_callback=print):
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
conn.execute("UPDATE markets SET extract_count = 0")
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log_callback("모든 추출 횟수가 초기화되었습니다.")
|
||||||
|
return "모든 추출 횟수가 초기화되었습니다."
|
||||||
|
|
||||||
|
def extract_bookmarks(self, country, grade, count, extract_based, max_extract, log_callback=print):
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect(self.db_path)
|
||||||
|
query = "SELECT id, mall_name AS name, mall_url AS url FROM markets WHERE 1=1"
|
||||||
|
if country != "랜덤":
|
||||||
|
query += f" AND country = '{country}'"
|
||||||
|
if grade != "랜덤":
|
||||||
|
query += f" AND mall_grade = '{grade}'"
|
||||||
|
if extract_based:
|
||||||
|
query += " AND extract_count < ? ORDER BY extract_count ASC, RANDOM()"
|
||||||
|
query += f" LIMIT {count}"
|
||||||
|
df = pd.read_sql_query(query, conn, params=(max_extract,))
|
||||||
|
else:
|
||||||
|
query += " ORDER BY RANDOM()"
|
||||||
|
query += f" LIMIT {count}"
|
||||||
|
df = pd.read_sql_query(query, conn)
|
||||||
|
if df.empty:
|
||||||
|
log_callback("추출 가능한 데이터가 없습니다.")
|
||||||
|
conn.close()
|
||||||
|
return None
|
||||||
|
for record in df.to_dict("records"):
|
||||||
|
conn.execute("UPDATE markets SET extract_count = extract_count + 1 WHERE id = ?", (record["id"],))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
log_callback(f"{len(df)}개의 북마크를 추출했습니다.")
|
||||||
|
return df.to_dict("records")
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"DB 쿼리 실행 오류: {e}"
|
||||||
|
log_callback(msg)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 북마크 추가 작업을 백그라운드로 처리하는 모듈 (BookmarkWorker)
|
||||||
|
# =====================================================================
|
||||||
|
class BookmarkWorker(threading.Thread):
|
||||||
|
def __init__(self, bookmarks, folder_name, bookmarks_path, browser_path, selected_browser, remove_existing,
|
||||||
|
progress_callback, log_callback, completed_callback):
|
||||||
|
super().__init__()
|
||||||
|
self.bookmarks = bookmarks
|
||||||
|
self.folder_name = folder_name
|
||||||
|
self.bookmarks_path = bookmarks_path
|
||||||
|
self.browser_path = browser_path
|
||||||
|
self.selected_browser = selected_browser
|
||||||
|
self.remove_existing = remove_existing
|
||||||
|
self.progress_callback = progress_callback
|
||||||
|
self.log_callback = log_callback
|
||||||
|
self.completed_callback = completed_callback
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
try:
|
||||||
|
if not os.path.exists(self.bookmarks_path):
|
||||||
|
self.log_callback(f"즐겨찾기 JSON 파일을 찾을 수 없습니다: {self.bookmarks_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.bookmarks_path, "r", encoding="utf-8") as file:
|
||||||
|
file_content = file.read().strip()
|
||||||
|
if not file_content:
|
||||||
|
bookmarks_data = {"roots": {"bookmark_bar": {"children": []}}}
|
||||||
|
self.log_callback("JSON 파일이 비어 있어 기본값으로 초기화합니다.")
|
||||||
|
else:
|
||||||
|
bookmarks_data = json.loads(file_content)
|
||||||
|
except json.JSONDecodeError as e:
|
||||||
|
self.log_callback(f"JSON 파싱 오류: {e}")
|
||||||
|
bookmarks_data = {"roots": {"bookmark_bar": {"children": []}}}
|
||||||
|
|
||||||
|
if self.remove_existing:
|
||||||
|
bookmarks_data["roots"]["bookmark_bar"] = self.remove_existing_bookmarks(
|
||||||
|
bookmarks_data["roots"]["bookmark_bar"]
|
||||||
|
)
|
||||||
|
|
||||||
|
total = len(self.bookmarks)
|
||||||
|
bookmark_bar = bookmarks_data["roots"]["bookmark_bar"]
|
||||||
|
current_time = datetime.now().strftime("%m-%d-%H-%M-%S")
|
||||||
|
parent_folder_name = f"거상북마크-{current_time}"
|
||||||
|
parent_folder = {"type": "folder", "name": parent_folder_name, "children": []}
|
||||||
|
|
||||||
|
chunk_size = 100
|
||||||
|
for idx, start in enumerate(range(0, total, chunk_size), start=1):
|
||||||
|
sub_folder = {
|
||||||
|
"type": "folder",
|
||||||
|
"name": f"거상북마크-{self.folder_name}-{idx}",
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
for bm in self.bookmarks[start:start+chunk_size]:
|
||||||
|
sub_folder["children"].append({
|
||||||
|
"type": "url",
|
||||||
|
"name": bm["name"],
|
||||||
|
"url": bm["url"]
|
||||||
|
})
|
||||||
|
parent_folder["children"].append(sub_folder)
|
||||||
|
progress = int((start + min(chunk_size, total - start)) / total * 100)
|
||||||
|
self.progress_callback(progress)
|
||||||
|
|
||||||
|
bookmark_bar["children"].append(parent_folder)
|
||||||
|
with open(self.bookmarks_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(bookmarks_data, f, indent=4, ensure_ascii=False)
|
||||||
|
self.log_callback("즐겨찾기 추가 완료!")
|
||||||
|
|
||||||
|
self.run_browser_with_profile(self.browser_path, self.bookmarks_path, self.selected_browser)
|
||||||
|
self.run_and_focus_copyman()
|
||||||
|
self.completed_callback()
|
||||||
|
except Exception as e:
|
||||||
|
self.log_callback(f"작업 오류: {e}\n{traceback.format_exc()}")
|
||||||
|
self.progress_callback(0)
|
||||||
|
|
||||||
|
def run_browser_with_profile(self, browser_path, bookmarks_path, browser_name):
|
||||||
|
if "웨일" in browser_name:
|
||||||
|
for proc in psutil.process_iter(attrs=["pid", "name"]):
|
||||||
|
if "whale" in proc.info["name"].lower():
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
self.log_callback("웨일 프로세스 종료됨.")
|
||||||
|
except Exception as e:
|
||||||
|
self.log_callback(f"웨일 종료 오류: {e}")
|
||||||
|
if os.path.exists(browser_path):
|
||||||
|
profile_dir = os.path.basename(os.path.dirname(bookmarks_path))
|
||||||
|
if "크롬" in browser_name.lower():
|
||||||
|
page_url = "chrome://bookmarks/"
|
||||||
|
elif "웨일" in browser_name.lower():
|
||||||
|
page_url = "whale://bookmarks/"
|
||||||
|
else:
|
||||||
|
page_url = "about:blank"
|
||||||
|
if profile_dir:
|
||||||
|
subprocess.Popen([browser_path, f"--profile-directory={profile_dir}", page_url])
|
||||||
|
self.log_callback(f"{browser_name} 프로필 {profile_dir}로 북마크 열기")
|
||||||
|
else:
|
||||||
|
subprocess.Popen([browser_path, page_url])
|
||||||
|
self.log_callback(f"{browser_name} 기본 프로필로 북마크 열기")
|
||||||
|
else:
|
||||||
|
self.log_callback(f"{browser_name} 경로가 올바르지 않습니다: {browser_path}")
|
||||||
|
|
||||||
|
def run_and_focus_copyman(self):
|
||||||
|
program_name = "@카피맨.exe"
|
||||||
|
shortcut_name = "@카피맨"
|
||||||
|
window_title_start = "카피맨"
|
||||||
|
try:
|
||||||
|
pid = self.is_program_running(program_name)
|
||||||
|
if pid:
|
||||||
|
self.log_callback(f"{program_name} 실행 중 (PID: {pid})")
|
||||||
|
if not self.focus_window_by_title(window_title_start):
|
||||||
|
self.log_callback("카피맨 창을 찾지 못함.")
|
||||||
|
else:
|
||||||
|
self.log_callback("카피맨 실행 안됨. 실행 시도...")
|
||||||
|
shortcut = self.find_shortcut_in_start_menu(shortcut_name)
|
||||||
|
if shortcut:
|
||||||
|
self.run_program(shortcut)
|
||||||
|
else:
|
||||||
|
self.log_callback("카피맨 바로가기 없음.")
|
||||||
|
except Exception as e:
|
||||||
|
self.log_callback(f"카피맨 실행 오류: {e}\n{traceback.format_exc()}")
|
||||||
|
|
||||||
|
def is_program_running(self, process_name):
|
||||||
|
for proc in psutil.process_iter(attrs=["pid", "name"]):
|
||||||
|
if process_name.lower() in proc.info["name"].lower():
|
||||||
|
return proc.info["pid"]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def focus_window_by_title(self, title_start):
|
||||||
|
try:
|
||||||
|
import pygetwindow as gw
|
||||||
|
for window in gw.getAllWindows():
|
||||||
|
if window.title and window.title.startswith(title_start):
|
||||||
|
try:
|
||||||
|
window.activate()
|
||||||
|
self.log_callback(f"창 활성화: {window.title}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
self.log_callback(f"창 활성화 실패: {e}")
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
self.log_callback(f"포커스 전환 오류: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def find_shortcut_in_start_menu(self, shortcut_name):
|
||||||
|
user_menu = os.path.expandvars(r"%APPDATA%\Microsoft\Windows\Start Menu\Programs")
|
||||||
|
all_users_menu = os.path.expandvars(r"%ProgramData%\Microsoft\Windows\Start Menu\Programs")
|
||||||
|
for path in [user_menu, all_users_menu]:
|
||||||
|
shortcut = glob.glob(os.path.join(path, f"**\\{shortcut_name}.lnk"), recursive=True)
|
||||||
|
if shortcut:
|
||||||
|
return shortcut[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def run_program(self, shortcut_path):
|
||||||
|
try:
|
||||||
|
subprocess.Popen([shortcut_path], shell=True)
|
||||||
|
self.log_callback(f"프로그램 실행: {shortcut_path}")
|
||||||
|
except Exception as e:
|
||||||
|
self.log_callback(f"실행 오류: {e}")
|
||||||
|
|
||||||
|
def remove_existing_bookmarks(self, node):
|
||||||
|
if not isinstance(node, dict):
|
||||||
|
return node
|
||||||
|
if node.get("type") == "folder" and node.get("name", "").startswith("거상북마크"):
|
||||||
|
self.log_callback(f"제거된 폴더: {node.get('name')}")
|
||||||
|
return None
|
||||||
|
if "children" in node:
|
||||||
|
node["children"] = [self.remove_existing_bookmarks(child)
|
||||||
|
for child in node["children"]
|
||||||
|
if self.remove_existing_bookmarks(child) is not None]
|
||||||
|
return node
|
||||||
|
|
||||||
|
# 전역 DBHandler 생성
|
||||||
|
db_handler = DBHandler()
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# Eel을 통한 UI와 백엔드 연동
|
||||||
|
# =====================================================================
|
||||||
|
@eel.expose
|
||||||
|
def load_excel(file_path, remove_existing):
|
||||||
|
result = db_handler.load_excel(file_path, remove_existing, log_callback=print)
|
||||||
|
eel.log(result)
|
||||||
|
|
||||||
|
@eel.expose
|
||||||
|
def view_data():
|
||||||
|
html_data = db_handler.view_data()
|
||||||
|
eel.show_data(html_data)
|
||||||
|
|
||||||
|
@eel.expose
|
||||||
|
def reset_extract_count():
|
||||||
|
result = db_handler.reset_extract_count(log_callback=print)
|
||||||
|
eel.log(result)
|
||||||
|
|
||||||
|
@eel.expose
|
||||||
|
def run_task(country, grade, count, remove_existing, extract_based, max_extract, browser_choice, chrome_path, whale_path):
|
||||||
|
bookmarks = db_handler.extract_bookmarks(country, grade, int(count), extract_based, int(max_extract), log_callback=print)
|
||||||
|
if bookmarks is None:
|
||||||
|
eel.log("북마크 추출 실패")
|
||||||
|
return
|
||||||
|
folder_name = f"거상북마크-{grade}"
|
||||||
|
if browser_choice == "크롬":
|
||||||
|
# 기본 크롬 북마크 경로 (사용자 환경에 맞게 수정 필요)
|
||||||
|
selected_bookmarks_path = os.path.join(os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\User Data\Default"), "Bookmarks")
|
||||||
|
selected_browser_path = chrome_path
|
||||||
|
selected_browser = "크롬"
|
||||||
|
eel.log("크롬 브라우저 선택됨.")
|
||||||
|
else:
|
||||||
|
selected_bookmarks_path = os.path.join(os.path.expandvars(r"%LOCALAPPDATA%\Naver\Naver Whale\User Data\Default"), "Bookmarks")
|
||||||
|
selected_browser_path = whale_path
|
||||||
|
selected_browser = "웨일"
|
||||||
|
eel.log("웨일 브라우저 선택됨.")
|
||||||
|
if not os.path.exists(selected_browser_path):
|
||||||
|
eel.log("브라우저 실행 파일 경로가 유효하지 않습니다.")
|
||||||
|
return
|
||||||
|
if not os.path.exists(selected_bookmarks_path):
|
||||||
|
eel.log("브라우저 북마크 경로가 유효하지 않습니다.")
|
||||||
|
return
|
||||||
|
|
||||||
|
def progress_callback(val):
|
||||||
|
eel.update_progress(val)
|
||||||
|
def log_callback(msg):
|
||||||
|
eel.log(msg)
|
||||||
|
def completed_callback():
|
||||||
|
eel.log("작업 완료!")
|
||||||
|
eel.task_completed()
|
||||||
|
|
||||||
|
worker = BookmarkWorker(bookmarks, folder_name, selected_bookmarks_path,
|
||||||
|
selected_browser_path, selected_browser, remove_existing,
|
||||||
|
progress_callback, log_callback, completed_callback)
|
||||||
|
worker.start()
|
||||||
|
|
||||||
|
# Eel 초기화 (웹 폴더 내부에 index.html 파일이 있어야 합니다.)
|
||||||
|
eel.init("web")
|
||||||
|
|
||||||
|
# Eel 앱 실행 (index.html 열림)
|
||||||
|
eel.start("index.html", size=(900,700))
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import dearpygui.dearpygui as dpg
|
||||||
|
|
||||||
|
def button_callback(sender, app_data, user_data):
|
||||||
|
# 기존 로그 텍스트에 새로운 메시지를 추가
|
||||||
|
current_log = dpg.get_value("log_text")
|
||||||
|
new_log = current_log + "Hello, world!\n"
|
||||||
|
dpg.set_value("log_text", new_log)
|
||||||
|
|
||||||
|
dpg.create_context()
|
||||||
|
|
||||||
|
with dpg.window(label="Example Window", tag="main_window"):
|
||||||
|
dpg.add_text("DearPyGui 2.0 Immediate Mode Example")
|
||||||
|
dpg.add_button(label="Click Me", callback=button_callback)
|
||||||
|
# 멀티라인 InputText 위젯을 로그 영역으로 사용
|
||||||
|
dpg.add_input_text(label="Log", tag="log_text", multiline=True, height=100, width=400, readonly=True)
|
||||||
|
|
||||||
|
dpg.create_viewport(title='Example App', width=600, height=400)
|
||||||
|
dpg.setup_dearpygui()
|
||||||
|
dpg.show_viewport()
|
||||||
|
dpg.start_dearpygui()
|
||||||
|
dpg.destroy_context()
|
||||||
|
|
@ -0,0 +1,458 @@
|
||||||
|
import sys
|
||||||
|
import asyncio
|
||||||
|
from PySide6 import QtWidgets, QtCore, QtGui, QtWebEngineWidgets
|
||||||
|
from PySide6.QtCore import QUrl, QTimer
|
||||||
|
from PySide6.QtGui import QIcon, QAction
|
||||||
|
from PySide6.QtWidgets import QToolBar, QLineEdit, QMessageBox, QHBoxLayout, QWidget, QDialog, QFormLayout, QDialogButtonBox
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
|
||||||
|
# chrome_sync 모듈 import (플레이스홀더)
|
||||||
|
import chrome_sync
|
||||||
|
|
||||||
|
# Playwright 자동화 작업을 위한 QThread 클래스
|
||||||
|
class PlaywrightThread(QtCore.QThread):
|
||||||
|
result_signal = QtCore.Signal(str)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
asyncio.run(self.run_playwright())
|
||||||
|
|
||||||
|
async def run_playwright(self):
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(headless=False)
|
||||||
|
page = await browser.new_page()
|
||||||
|
await page.goto("https://example.com")
|
||||||
|
screenshot_path = "screenshot.png"
|
||||||
|
await page.screenshot(path=screenshot_path)
|
||||||
|
await browser.close()
|
||||||
|
self.result_signal.emit(f"Playwright 자동화 완료: 스크린샷 저장 - {screenshot_path}")
|
||||||
|
|
||||||
|
# 각 브라우저 탭에 들어갈 위젯 클래스 (QWebEngineView 포함)
|
||||||
|
class BrowserTab(QtWidgets.QWidget):
|
||||||
|
def __init__(self, url, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.web_view = QtWebEngineWidgets.QWebEngineView()
|
||||||
|
layout.addWidget(self.web_view)
|
||||||
|
self.web_view.setUrl(QUrl(url))
|
||||||
|
|
||||||
|
# 북마크 수정/추가 다이얼로그
|
||||||
|
class BookmarkEditDialog(QDialog):
|
||||||
|
def __init__(self, bookmark=None, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setWindowTitle("북마크 수정" if bookmark else "새 북마크 추가")
|
||||||
|
layout = QFormLayout(self)
|
||||||
|
self.name_edit = QLineEdit()
|
||||||
|
self.url_edit = QLineEdit()
|
||||||
|
self.folder_edit = QLineEdit()
|
||||||
|
if bookmark:
|
||||||
|
self.name_edit.setText(bookmark["name"])
|
||||||
|
self.url_edit.setText(bookmark["url"])
|
||||||
|
self.folder_edit.setText(bookmark.get("folder", ""))
|
||||||
|
layout.addRow("이름:", self.name_edit)
|
||||||
|
layout.addRow("주소:", self.url_edit)
|
||||||
|
layout.addRow("폴더 경로:", self.folder_edit)
|
||||||
|
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
|
button_box.accepted.connect(self.accept)
|
||||||
|
button_box.rejected.connect(self.reject)
|
||||||
|
layout.addWidget(button_box)
|
||||||
|
|
||||||
|
def getData(self):
|
||||||
|
return {
|
||||||
|
"name": self.name_edit.text().strip(),
|
||||||
|
"url": self.url_edit.text().strip(),
|
||||||
|
"folder": self.folder_edit.text().strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
# 북마크 버튼 (QToolButton 확장) - 우클릭 시 수정 메뉴 제공
|
||||||
|
class BookmarkButton(QtWidgets.QToolButton):
|
||||||
|
def __init__(self, bookmark, edit_callback, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.bookmark = bookmark
|
||||||
|
self.edit_callback = edit_callback
|
||||||
|
self.setText(bookmark["name"])
|
||||||
|
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||||
|
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||||
|
|
||||||
|
def show_context_menu(self, pos):
|
||||||
|
menu = QtWidgets.QMenu(self)
|
||||||
|
edit_action = menu.addAction("북마크 수정")
|
||||||
|
action = menu.exec(self.mapToGlobal(pos))
|
||||||
|
if action == edit_action:
|
||||||
|
self.edit_callback(self)
|
||||||
|
|
||||||
|
# 도구 패널(QTabWidget) - 세로 탭
|
||||||
|
class ToolPanel(QtWidgets.QTabWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTabPosition(QtWidgets.QTabWidget.West)
|
||||||
|
self.setMinimumWidth(250)
|
||||||
|
self.auto_hide = False
|
||||||
|
self.hide_timer = QTimer(self)
|
||||||
|
self.hide_timer.setInterval(500)
|
||||||
|
self.hide_timer.setSingleShot(True)
|
||||||
|
self.hide_timer.timeout.connect(self.hide_panel)
|
||||||
|
self.installEventFilter(self)
|
||||||
|
|
||||||
|
def eventFilter(self, obj, event):
|
||||||
|
if self.auto_hide:
|
||||||
|
if event.type() == QtCore.QEvent.Leave:
|
||||||
|
self.hide_timer.start()
|
||||||
|
elif event.type() == QtCore.QEvent.Enter:
|
||||||
|
self.hide_timer.stop()
|
||||||
|
return super().eventFilter(obj, event)
|
||||||
|
|
||||||
|
def hide_panel(self):
|
||||||
|
self.hide()
|
||||||
|
|
||||||
|
def show_panel(self):
|
||||||
|
self.show()
|
||||||
|
|
||||||
|
# 문자전송 탭의 간단한 예제 위젯
|
||||||
|
class MessageTab(QtWidgets.QWidget):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
layout.addWidget(QtWidgets.QLabel("문자전송 기능"))
|
||||||
|
self.message_input = QtWidgets.QLineEdit()
|
||||||
|
self.message_input.setPlaceholderText("보낼 메시지를 입력하세요")
|
||||||
|
self.send_button = QtWidgets.QPushButton("전송")
|
||||||
|
self.send_button.clicked.connect(self.send_message)
|
||||||
|
layout.addWidget(self.message_input)
|
||||||
|
layout.addWidget(self.send_button)
|
||||||
|
self.status_label = QtWidgets.QLabel("")
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
def send_message(self):
|
||||||
|
message = self.message_input.text()
|
||||||
|
# 실제 문자 전송 API 호출 구현 필요
|
||||||
|
self.status_label.setText(f"전송됨: {message}")
|
||||||
|
|
||||||
|
# 메인 윈도우 클래스
|
||||||
|
class MainWindow(QtWidgets.QMainWindow):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.setWindowTitle("크롬과 유사한 커스텀 브라우저")
|
||||||
|
self.resize(1920, 1080)
|
||||||
|
|
||||||
|
# 북마크 리스트 (각 항목은 dict: name, url, folder)
|
||||||
|
self.bookmarks = []
|
||||||
|
|
||||||
|
# 메인 레이아웃: 브라우저 영역과 도구 패널 (세로 탭)
|
||||||
|
central_widget = QtWidgets.QWidget()
|
||||||
|
self.setCentralWidget(central_widget)
|
||||||
|
self.main_layout = QtWidgets.QHBoxLayout(central_widget)
|
||||||
|
self.main_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# 브라우저 영역 (네비게이션 바, 북마크 바, 브라우저 탭)
|
||||||
|
self.browser_area = QtWidgets.QWidget()
|
||||||
|
self.init_browser_area()
|
||||||
|
self.main_layout.addWidget(self.browser_area, 8)
|
||||||
|
|
||||||
|
# 도구 패널
|
||||||
|
self.tool_panel = ToolPanel()
|
||||||
|
self.init_tool_panel()
|
||||||
|
self.main_layout.addWidget(self.tool_panel, 2)
|
||||||
|
|
||||||
|
def init_browser_area(self):
|
||||||
|
layout = QtWidgets.QVBoxLayout(self.browser_area)
|
||||||
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
layout.setSpacing(0)
|
||||||
|
|
||||||
|
# 1. 네비게이션 바 (아이콘 버튼 + 주소창 등)
|
||||||
|
self.nav_toolbar = QToolBar()
|
||||||
|
self.nav_toolbar.setIconSize(QtCore.QSize(24, 24))
|
||||||
|
self.nav_toolbar.setMovable(False)
|
||||||
|
layout.addWidget(self.nav_toolbar)
|
||||||
|
|
||||||
|
# 뒤로가기
|
||||||
|
back_icon = self.style().standardIcon(QtWidgets.QStyle.SP_ArrowBack)
|
||||||
|
back_action = QAction(back_icon, "", self)
|
||||||
|
back_action.triggered.connect(self.navigate_back)
|
||||||
|
self.nav_toolbar.addAction(back_action)
|
||||||
|
|
||||||
|
# 앞으로가기
|
||||||
|
forward_icon = self.style().standardIcon(QtWidgets.QStyle.SP_ArrowForward)
|
||||||
|
forward_action = QAction(forward_icon, "", self)
|
||||||
|
forward_action.triggered.connect(self.navigate_forward)
|
||||||
|
self.nav_toolbar.addAction(forward_action)
|
||||||
|
|
||||||
|
# 새로고침
|
||||||
|
refresh_icon = self.style().standardIcon(QtWidgets.QStyle.SP_BrowserReload)
|
||||||
|
refresh_action = QAction(refresh_icon, "", self)
|
||||||
|
refresh_action.triggered.connect(self.refresh_page)
|
||||||
|
self.nav_toolbar.addAction(refresh_action)
|
||||||
|
|
||||||
|
# 주소창
|
||||||
|
self.url_bar = QLineEdit()
|
||||||
|
self.url_bar.returnPressed.connect(self.load_url_from_bar)
|
||||||
|
self.url_bar.setMinimumWidth(400)
|
||||||
|
self.nav_toolbar.addWidget(self.url_bar)
|
||||||
|
|
||||||
|
self.nav_toolbar.addSeparator()
|
||||||
|
|
||||||
|
# 북마크 추가 (아이콘)
|
||||||
|
bookmark_icon = self.style().standardIcon(QtWidgets.QStyle.SP_DialogYesButton)
|
||||||
|
bookmark_action = QAction(bookmark_icon, "", self)
|
||||||
|
bookmark_action.triggered.connect(self.new_bookmark)
|
||||||
|
self.nav_toolbar.addAction(bookmark_action)
|
||||||
|
|
||||||
|
# 확장 프로그램 (아이콘)
|
||||||
|
ext_icon = self.style().standardIcon(QtWidgets.QStyle.SP_DirIcon)
|
||||||
|
ext_action = QAction(ext_icon, "", self)
|
||||||
|
ext_action.triggered.connect(self.show_extensions)
|
||||||
|
self.nav_toolbar.addAction(ext_action)
|
||||||
|
|
||||||
|
# 2. 북마크 바 (주소창 바로 아래) - 오른쪽 끝에 도구 패널 토글 버튼 포함
|
||||||
|
self.bookmark_bar_widget = QWidget()
|
||||||
|
bookmark_layout = QHBoxLayout(self.bookmark_bar_widget)
|
||||||
|
bookmark_layout.setContentsMargins(2, 2, 2, 2)
|
||||||
|
self.bookmark_toolbar = QToolBar()
|
||||||
|
self.bookmark_toolbar.setMovable(False)
|
||||||
|
self.bookmark_toolbar.setIconSize(QtCore.QSize(20, 20))
|
||||||
|
bookmark_layout.addWidget(self.bookmark_toolbar)
|
||||||
|
bookmark_layout.addStretch()
|
||||||
|
self.tool_toggle_button = QtWidgets.QToolButton()
|
||||||
|
toggle_icon = self.style().standardIcon(QtWidgets.QStyle.SP_ComputerIcon)
|
||||||
|
self.tool_toggle_button.setIcon(toggle_icon)
|
||||||
|
self.tool_toggle_button.setCheckable(True)
|
||||||
|
self.tool_toggle_button.setChecked(True)
|
||||||
|
self.tool_toggle_button.clicked.connect(self.toggle_tool_panel)
|
||||||
|
self.tool_toggle_button.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||||
|
self.tool_toggle_button.customContextMenuRequested.connect(self.show_toggle_menu)
|
||||||
|
bookmark_layout.addWidget(self.tool_toggle_button)
|
||||||
|
layout.addWidget(self.bookmark_bar_widget)
|
||||||
|
self.load_sync_bookmarks()
|
||||||
|
|
||||||
|
# 3. 브라우저 탭 영역
|
||||||
|
self.browser_tabs = QtWidgets.QTabWidget()
|
||||||
|
self.browser_tabs.setTabsClosable(True)
|
||||||
|
self.browser_tabs.tabCloseRequested.connect(self.close_current_tab)
|
||||||
|
self.browser_tabs.currentChanged.connect(self.update_url_bar)
|
||||||
|
layout.addWidget(self.browser_tabs)
|
||||||
|
|
||||||
|
# 첫 탭 추가
|
||||||
|
self.add_new_tab("https://www.google.com", "Google")
|
||||||
|
|
||||||
|
def init_tool_panel(self):
|
||||||
|
# 도구 패널 탭 추가 (세로 탭)
|
||||||
|
# 배송비 계산기 탭
|
||||||
|
self.shipping_tab = QtWidgets.QWidget()
|
||||||
|
self.init_shipping_tab()
|
||||||
|
self.tool_panel.addTab(self.shipping_tab, "배송비")
|
||||||
|
# 금지어 관리 탭
|
||||||
|
self.prohibited_tab = QtWidgets.QWidget()
|
||||||
|
self.init_prohibited_tab()
|
||||||
|
self.tool_panel.addTab(self.prohibited_tab, "금지어")
|
||||||
|
# 카테고리 관리 탭
|
||||||
|
self.category_tab = QtWidgets.QWidget()
|
||||||
|
self.init_category_tab()
|
||||||
|
self.tool_panel.addTab(self.category_tab, "카테고리")
|
||||||
|
# Playwright 매크로 탭
|
||||||
|
self.playwright_tab = QtWidgets.QWidget()
|
||||||
|
self.init_playwright_tab()
|
||||||
|
self.tool_panel.addTab(self.playwright_tab, "Playwright")
|
||||||
|
# 문자전송 탭
|
||||||
|
self.message_tab = MessageTab()
|
||||||
|
self.tool_panel.addTab(self.message_tab, "문자전송")
|
||||||
|
|
||||||
|
def add_new_tab(self, url="https://www.google.com", label="New Tab"):
|
||||||
|
new_tab = BrowserTab(url)
|
||||||
|
index = self.browser_tabs.addTab(new_tab, label)
|
||||||
|
self.browser_tabs.setCurrentIndex(index)
|
||||||
|
new_tab.web_view.urlChanged.connect(lambda qurl, tab=new_tab: self.update_tab_title(tab, qurl))
|
||||||
|
new_tab.web_view.loadFinished.connect(lambda ok, tab=new_tab: self.update_url_bar())
|
||||||
|
|
||||||
|
def close_current_tab(self, index):
|
||||||
|
if self.browser_tabs.count() > 1:
|
||||||
|
self.browser_tabs.removeTab(index)
|
||||||
|
|
||||||
|
def update_tab_title(self, tab, qurl):
|
||||||
|
title = tab.web_view.title() or qurl.toString()
|
||||||
|
index = self.browser_tabs.indexOf(tab)
|
||||||
|
self.browser_tabs.setTabText(index, title)
|
||||||
|
if self.browser_tabs.currentWidget() == tab:
|
||||||
|
self.url_bar.setText(qurl.toString())
|
||||||
|
|
||||||
|
def update_url_bar(self):
|
||||||
|
current_tab = self.browser_tabs.currentWidget()
|
||||||
|
if current_tab:
|
||||||
|
url = current_tab.web_view.url().toString()
|
||||||
|
self.url_bar.setText(url)
|
||||||
|
|
||||||
|
def load_url_from_bar(self):
|
||||||
|
url_text = self.url_bar.text().strip()
|
||||||
|
if url_text and not url_text.startswith("http"):
|
||||||
|
url_text = "http://" + url_text
|
||||||
|
current_tab = self.browser_tabs.currentWidget()
|
||||||
|
if current_tab:
|
||||||
|
current_tab.web_view.setUrl(QUrl(url_text))
|
||||||
|
|
||||||
|
def navigate_back(self):
|
||||||
|
current_tab = self.browser_tabs.currentWidget()
|
||||||
|
if current_tab:
|
||||||
|
current_tab.web_view.back()
|
||||||
|
|
||||||
|
def navigate_forward(self):
|
||||||
|
current_tab = self.browser_tabs.currentWidget()
|
||||||
|
if current_tab:
|
||||||
|
current_tab.web_view.forward()
|
||||||
|
|
||||||
|
def refresh_page(self):
|
||||||
|
current_tab = self.browser_tabs.currentWidget()
|
||||||
|
if current_tab:
|
||||||
|
current_tab.web_view.reload()
|
||||||
|
|
||||||
|
# 북마크 추가 버튼 클릭 시: 새 북마크 다이얼로그 띄움
|
||||||
|
def new_bookmark(self):
|
||||||
|
dialog = BookmarkEditDialog(parent=self)
|
||||||
|
if dialog.exec() == QDialog.Accepted:
|
||||||
|
data = dialog.getData()
|
||||||
|
self.bookmarks.append(data)
|
||||||
|
self.add_bookmark_button(data)
|
||||||
|
|
||||||
|
# 북마크 바에 버튼 추가
|
||||||
|
def add_bookmark_button(self, bookmark):
|
||||||
|
btn = BookmarkButton(bookmark, edit_callback=self.edit_bookmark)
|
||||||
|
btn.clicked.connect(lambda checked, url=bookmark["url"]: self.open_bookmark(url))
|
||||||
|
self.bookmark_toolbar.addWidget(btn)
|
||||||
|
|
||||||
|
# 우클릭으로 북마크 수정 호출
|
||||||
|
def edit_bookmark(self, btn: BookmarkButton):
|
||||||
|
dialog = BookmarkEditDialog(bookmark=btn.bookmark, parent=self)
|
||||||
|
if dialog.exec() == QDialog.Accepted:
|
||||||
|
data = dialog.getData()
|
||||||
|
btn.bookmark = data
|
||||||
|
btn.setText(data["name"])
|
||||||
|
# 업데이트된 데이터를 bookmarks 리스트에도 반영
|
||||||
|
for bm in self.bookmarks:
|
||||||
|
if bm["url"] == btn.bookmark["url"]:
|
||||||
|
bm.update(data)
|
||||||
|
break
|
||||||
|
|
||||||
|
def open_bookmark(self, url):
|
||||||
|
current_tab = self.browser_tabs.currentWidget()
|
||||||
|
if current_tab:
|
||||||
|
current_tab.web_view.setUrl(QUrl(url))
|
||||||
|
|
||||||
|
def load_sync_bookmarks(self):
|
||||||
|
# chrome_sync 모듈을 통해 동기화된 북마크 가져오기 (플레이스홀더)
|
||||||
|
sync_bookmarks = chrome_sync.get_chrome_bookmarks()
|
||||||
|
for bm in sync_bookmarks:
|
||||||
|
if bm not in self.bookmarks:
|
||||||
|
self.bookmarks.append(bm)
|
||||||
|
self.add_bookmark_button(bm)
|
||||||
|
|
||||||
|
def show_extensions(self):
|
||||||
|
# chrome_sync 모듈을 통해 확장 프로그램 가져오기 (플레이스홀더)
|
||||||
|
ext_list = chrome_sync.get_chrome_extensions()
|
||||||
|
msg = "\n".join(ext_list)
|
||||||
|
QMessageBox.information(self, "확장 프로그램", f"사용 가능한 확장 프로그램:\n{msg}")
|
||||||
|
|
||||||
|
def init_shipping_tab(self):
|
||||||
|
layout = QtWidgets.QVBoxLayout(self.shipping_tab)
|
||||||
|
layout.addWidget(QtWidgets.QLabel("배송비 계산기"))
|
||||||
|
self.weight_input = QtWidgets.QLineEdit()
|
||||||
|
self.weight_input.setPlaceholderText("무게 입력")
|
||||||
|
self.size_input = QtWidgets.QLineEdit()
|
||||||
|
self.size_input.setPlaceholderText("크기 입력")
|
||||||
|
self.calculate_button = QtWidgets.QPushButton("계산")
|
||||||
|
self.calculate_result = QtWidgets.QLabel("결과:")
|
||||||
|
self.calculate_button.clicked.connect(self.calculate_shipping)
|
||||||
|
layout.addWidget(self.weight_input)
|
||||||
|
layout.addWidget(self.size_input)
|
||||||
|
layout.addWidget(self.calculate_button)
|
||||||
|
layout.addWidget(self.calculate_result)
|
||||||
|
|
||||||
|
def calculate_shipping(self):
|
||||||
|
try:
|
||||||
|
shipping_cost = float(self.weight_input.text()) * 0.5 + float(self.size_input.text()) * 0.3
|
||||||
|
self.calculate_result.setText(f"결과: {shipping_cost:.2f}")
|
||||||
|
except ValueError:
|
||||||
|
self.calculate_result.setText("올바른 숫자를 입력하세요.")
|
||||||
|
|
||||||
|
def init_prohibited_tab(self):
|
||||||
|
layout = QtWidgets.QVBoxLayout(self.prohibited_tab)
|
||||||
|
layout.addWidget(QtWidgets.QLabel("금지어 관리"))
|
||||||
|
self.prohibited_table = QtWidgets.QTableWidget(0, 3)
|
||||||
|
self.prohibited_table.setHorizontalHeaderLabels(["단어", "등급", "비고"])
|
||||||
|
layout.addWidget(self.prohibited_table)
|
||||||
|
btn_layout = QtWidgets.QHBoxLayout()
|
||||||
|
self.add_word_button = QtWidgets.QPushButton("추가")
|
||||||
|
self.remove_word_button = QtWidgets.QPushButton("삭제")
|
||||||
|
btn_layout.addWidget(self.add_word_button)
|
||||||
|
btn_layout.addWidget(self.remove_word_button)
|
||||||
|
layout.addLayout(btn_layout)
|
||||||
|
self.add_word_button.clicked.connect(self.add_prohibited_word)
|
||||||
|
self.remove_word_button.clicked.connect(self.remove_prohibited_word)
|
||||||
|
|
||||||
|
def add_prohibited_word(self):
|
||||||
|
row = self.prohibited_table.rowCount()
|
||||||
|
self.prohibited_table.insertRow(row)
|
||||||
|
self.prohibited_table.setItem(row, 0, QtWidgets.QTableWidgetItem("dummy_word"))
|
||||||
|
self.prohibited_table.setItem(row, 1, QtWidgets.QTableWidgetItem("1"))
|
||||||
|
self.prohibited_table.setItem(row, 2, QtWidgets.QTableWidgetItem("추가됨"))
|
||||||
|
|
||||||
|
def remove_prohibited_word(self):
|
||||||
|
row = self.prohibited_table.currentRow()
|
||||||
|
if row >= 0:
|
||||||
|
self.prohibited_table.removeRow(row)
|
||||||
|
|
||||||
|
def init_category_tab(self):
|
||||||
|
layout = QtWidgets.QVBoxLayout(self.category_tab)
|
||||||
|
layout.addWidget(QtWidgets.QLabel("카테고리 관리"))
|
||||||
|
self.category_table = QtWidgets.QTableWidget(0, 4)
|
||||||
|
self.category_table.setHorizontalHeaderLabels(["카테고리", "필터링", "금지여부", "추가배송비"])
|
||||||
|
layout.addWidget(self.category_table)
|
||||||
|
for cat in ["전자제품", "의류", "식품"]:
|
||||||
|
row = self.category_table.rowCount()
|
||||||
|
self.category_table.insertRow(row)
|
||||||
|
self.category_table.setItem(row, 0, QtWidgets.QTableWidgetItem(cat))
|
||||||
|
self.category_table.setItem(row, 1, QtWidgets.QTableWidgetItem("필터링"))
|
||||||
|
self.category_table.setItem(row, 2, QtWidgets.QTableWidgetItem("미금지"))
|
||||||
|
self.category_table.setItem(row, 3, QtWidgets.QTableWidgetItem("0"))
|
||||||
|
|
||||||
|
def init_playwright_tab(self):
|
||||||
|
layout = QtWidgets.QVBoxLayout(self.playwright_tab)
|
||||||
|
layout.addWidget(QtWidgets.QLabel("Playwright 매크로"))
|
||||||
|
self.playwright_run_button = QtWidgets.QPushButton("실행")
|
||||||
|
self.playwright_output = QtWidgets.QTextEdit()
|
||||||
|
self.playwright_output.setReadOnly(True)
|
||||||
|
layout.addWidget(self.playwright_run_button)
|
||||||
|
layout.addWidget(self.playwright_output)
|
||||||
|
self.playwright_run_button.clicked.connect(self.run_playwright_macro)
|
||||||
|
|
||||||
|
def run_playwright_macro(self):
|
||||||
|
self.playwright_run_button.setEnabled(False)
|
||||||
|
self.playwright_output.append("Playwright 실행 중...")
|
||||||
|
self.thread = PlaywrightThread()
|
||||||
|
self.thread.result_signal.connect(self.handle_playwright_result)
|
||||||
|
self.thread.finished.connect(lambda: self.playwright_run_button.setEnabled(True))
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def handle_playwright_result(self, result):
|
||||||
|
self.playwright_output.append(result)
|
||||||
|
|
||||||
|
def toggle_tool_panel(self):
|
||||||
|
if self.tool_toggle_button.isChecked():
|
||||||
|
self.tool_panel.show_panel()
|
||||||
|
else:
|
||||||
|
self.tool_panel.hide()
|
||||||
|
|
||||||
|
def show_toggle_menu(self, pos):
|
||||||
|
menu = QtWidgets.QMenu()
|
||||||
|
action = QtWidgets.QAction("자동 숨김 모드", self)
|
||||||
|
action.setCheckable(True)
|
||||||
|
action.setChecked(self.tool_panel.auto_hide)
|
||||||
|
action.triggered.connect(self.toggle_auto_hide)
|
||||||
|
menu.addAction(action)
|
||||||
|
menu.exec(self.tool_toggle_button.mapToGlobal(pos))
|
||||||
|
|
||||||
|
def toggle_auto_hide(self, checked):
|
||||||
|
self.tool_panel.auto_hide = checked
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
window = MainWindow()
|
||||||
|
window.show()
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>북마크 추가 앱 (Eel 버전)</title>
|
||||||
|
<script type="text/javascript" src="/eel.js"></script>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||||
|
.log { white-space: pre-wrap; background: #f0f0f0; padding: 10px; height: 200px; overflow: auto; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>북마크 추가 앱 (Eel 버전)</h1>
|
||||||
|
<!-- 비밀번호 입력 -->
|
||||||
|
<div>
|
||||||
|
<label for="password">비밀번호:</label>
|
||||||
|
<input type="password" id="password">
|
||||||
|
<button onclick="checkPassword()">확인</button>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<!-- 메인 콘텐츠 (비밀번호가 일치할 때 보임) -->
|
||||||
|
<div id="main-content" style="display:none;">
|
||||||
|
<h2>DB 입력</h2>
|
||||||
|
<input type="text" id="excelPath" placeholder="엑셀 파일 경로">
|
||||||
|
<input type="checkbox" id="removeExistingDB"> 기존 DB 제거
|
||||||
|
<button onclick="loadExcel()">엑셀 로드</button>
|
||||||
|
<br><br>
|
||||||
|
<button onclick="viewData()">데이터 보기</button>
|
||||||
|
<button onclick="resetExtractCount()">추출 횟수 초기화</button>
|
||||||
|
<hr>
|
||||||
|
<h2>북마크 추가 옵션</h2>
|
||||||
|
<div>
|
||||||
|
<label>국가:</label>
|
||||||
|
<select id="country">
|
||||||
|
<option>미국</option>
|
||||||
|
<option>유럽</option>
|
||||||
|
<option selected>중국</option>
|
||||||
|
<option>일본</option>
|
||||||
|
<option>한국</option>
|
||||||
|
<option>기타</option>
|
||||||
|
<option>랜덤</option>
|
||||||
|
</select>
|
||||||
|
<label>등급:</label>
|
||||||
|
<select id="grade">
|
||||||
|
<option>일반</option>
|
||||||
|
<option>파워</option>
|
||||||
|
<option>빅파워</option>
|
||||||
|
<option selected>랜덤</option>
|
||||||
|
</select>
|
||||||
|
<label>갯수:</label>
|
||||||
|
<input type="number" id="count" value="1000">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="removeExistingBookmarks">
|
||||||
|
<label>기존 북마크 제거</label>
|
||||||
|
<input type="checkbox" id="extractBased">
|
||||||
|
<label>추출 횟수 기반 추출</label>
|
||||||
|
<label>최대 추출 횟수:</label>
|
||||||
|
<input type="number" id="maxExtract" value="1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>브라우저 선택:</label>
|
||||||
|
<select id="browserChoice">
|
||||||
|
<option>크롬</option>
|
||||||
|
<option>웨일</option>
|
||||||
|
</select>
|
||||||
|
<br>
|
||||||
|
<label>크롬 경로:</label>
|
||||||
|
<input type="text" id="chromePath" placeholder="크롬 실행 파일 경로">
|
||||||
|
<br>
|
||||||
|
<label>웨일 경로:</label>
|
||||||
|
<input type="text" id="whalePath" placeholder="웨일 실행 파일 경로">
|
||||||
|
</div>
|
||||||
|
<button onclick="runTask()">실행</button>
|
||||||
|
<hr>
|
||||||
|
<h2>로그</h2>
|
||||||
|
<div class="log" id="log"></div>
|
||||||
|
<div>
|
||||||
|
<label>진행률:</label>
|
||||||
|
<progress id="progressBar" value="0" max="100"></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 데이터 보기 결과 (간단한 팝업) -->
|
||||||
|
<div id="dataDialog" style="display:none; border:1px solid #ccc; padding:10px;"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function checkPassword() {
|
||||||
|
let pwd = document.getElementById("password").value;
|
||||||
|
if(pwd !== "365") {
|
||||||
|
alert("비밀번호가 일치하지 않습니다. 프로그램을 종료합니다.");
|
||||||
|
window.close();
|
||||||
|
} else {
|
||||||
|
document.getElementById("main-content").style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function loadExcel() {
|
||||||
|
let path = document.getElementById("excelPath").value;
|
||||||
|
let removeExisting = document.getElementById("removeExistingDB").checked;
|
||||||
|
eel.load_excel(path, removeExisting);
|
||||||
|
}
|
||||||
|
function viewData() {
|
||||||
|
eel.view_data();
|
||||||
|
}
|
||||||
|
eel.expose(show_data);
|
||||||
|
function show_data(htmlData) {
|
||||||
|
let dlg = document.getElementById("dataDialog");
|
||||||
|
dlg.innerHTML = htmlData;
|
||||||
|
dlg.style.display = "block";
|
||||||
|
setTimeout(()=>{ dlg.style.display = "none"; }, 5000);
|
||||||
|
}
|
||||||
|
function resetExtractCount() {
|
||||||
|
eel.reset_extract_count();
|
||||||
|
}
|
||||||
|
function runTask() {
|
||||||
|
let country = document.getElementById("country").value;
|
||||||
|
let grade = document.getElementById("grade").value;
|
||||||
|
let count = document.getElementById("count").value;
|
||||||
|
let removeExisting = document.getElementById("removeExistingBookmarks").checked;
|
||||||
|
let extractBased = document.getElementById("extractBased").checked;
|
||||||
|
let maxExtract = document.getElementById("maxExtract").value;
|
||||||
|
let browserChoice = document.getElementById("browserChoice").value;
|
||||||
|
let chromePath = document.getElementById("chromePath").value;
|
||||||
|
let whalePath = document.getElementById("whalePath").value;
|
||||||
|
eel.run_task(country, grade, count, removeExisting, extractBased, maxExtract, browserChoice, chromePath, whalePath);
|
||||||
|
}
|
||||||
|
eel.expose(log);
|
||||||
|
function log(msg) {
|
||||||
|
let logDiv = document.getElementById("log");
|
||||||
|
logDiv.innerText += msg + "\n";
|
||||||
|
}
|
||||||
|
eel.expose(update_progress);
|
||||||
|
function update_progress(val) {
|
||||||
|
document.getElementById("progressBar").value = val;
|
||||||
|
}
|
||||||
|
eel.expose(task_completed);
|
||||||
|
function task_completed() {
|
||||||
|
alert("작업이 완료되었습니다!");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue