From c56dcd8a29908bca834a786322c0ae57789f9c29 Mon Sep 17 00:00:00 2001 From: Envy_PC Date: Thu, 27 Mar 2025 15:03:19 +0900 Subject: [PATCH] . --- markets.db | Bin 3858432 -> 3858432 bytes t2.py | 440 +++++++++++------- test/__pycache__/chrome_sync.cpython-312.pyc | Bin 0 -> 902 bytes test/chrome_sync.py | 20 + test/t3.py | 370 +++++++++++++++ test/t4.py | 21 + test/t5.py | 458 +++++++++++++++++++ web/index.html | 142 ++++++ 8 files changed, 1289 insertions(+), 162 deletions(-) create mode 100644 test/__pycache__/chrome_sync.cpython-312.pyc create mode 100644 test/chrome_sync.py create mode 100644 test/t3.py create mode 100644 test/t4.py create mode 100644 test/t5.py create mode 100644 web/index.html diff --git a/markets.db b/markets.db index 67c6fd9d9dcd7a47aa56276af3ac390972954ba6..a0a964834136426e3f9daa8487ff2a229bbc3dfe 100644 GIT binary patch delta 15405 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 sgX!NxdwlX{C7VP2C(38}A6P_If&c&j delta 18299 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 c%hTD%v4l8@+&uk*Hn^)f+d 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): country = country_dropdown.value grade = grade_dropdown.value try: - count = int(count_field.value) + cnt = int(count_field.value) except: - count = 1000 + cnt = 1000 remove_existing = remove_existing_checkbox.value extract_based = extract_based_checkbox.value try: @@ -344,82 +501,41 @@ def main(page: ft.Page): except: max_extract = 1 - # DB에서 조건에 맞는 데이터 추출 (간략화된 예제) - try: - 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)}") + bookmarks_extracted = db_handler.extract_bookmarks(country, grade, cnt, extract_based, max_extract, log) + if bookmarks_extracted is None: return - + nonlocal bookmarks_global + bookmarks_global = bookmarks_extracted folder_name = f"거상북마크-{grade}" if browser_dropdown.value == "크롬": selected_bookmarks_path = chrome_bookmarks_path selected_browser_path = chrome_path selected_browser = "크롬" - log("크롬 브라우저가 선택되었습니다.") + log("크롬 브라우저 선택됨.") else: selected_bookmarks_path = whale_bookmarks_path selected_browser_path = whale_path selected_browser = "웨일" - log("웨일 브라우저가 선택되었습니다.") - + log("웨일 브라우저 선택됨.") if not os.path.exists(selected_browser_path): - log("선택된 브라우저 실행 파일 경로가 유효하지 않습니다.") + log("브라우저 실행 파일 경로가 유효하지 않습니다.") return if not os.path.exists(selected_bookmarks_path): - log("선택된 브라우저의 북마크 경로가 유효하지 않습니다.") + log("브라우저 북마크 경로가 유효하지 않습니다.") return - - worker = BookmarkWorker( - bookmarks, folder_name, selected_bookmarks_path, - selected_browser_path, selected_browser, remove_existing, - update_progress, log, task_completed - ) + worker = BookmarkWorker(bookmarks_global, folder_name, selected_bookmarks_path, + selected_browser_path, selected_browser, remove_existing, + update_progress, log, task_completed) worker.start() run_button.on_click = run_task - # --- 레이아웃 구성 --- - controls_row = ft.Row(controls=[ - country_dropdown, - 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]) + controls_row = ft.Row(controls=[country_dropdown, grade_dropdown, count_field, remove_existing_checkbox]) + extract_row = ft.Row(controls=[extract_based_checkbox, max_extract_field, reset_extract_button]) + browser_row = ft.Row(controls=[browser_dropdown, chrome_path_button, whale_path_button, browser_path_field]) action_row = ft.Row(controls=[db_input_button, view_data_button, run_button]) - page.add( - controls_row, - extract_row, - browser_row, - action_row, - progress_bar, - ft.Text("로그:", size=14), - log_display - ) - + page.add(controls_row, extract_row, browser_row, action_row, progress_bar, ft.Text("로그:"), log_text) page.update() ft.app(target=main) diff --git a/test/__pycache__/chrome_sync.cpython-312.pyc b/test/__pycache__/chrome_sync.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bfa950ac59f0aea6524becdc936417d321c5a350 GIT binary patch literal 902 zcmb7DO-LJ25PqADn@xKq6YSYVi=Pv>saMvCxa&WUnn0_PvE*Zk{@CRj@)29eA^|^S=3J=VyQP^#u^f z(C=T#=R86`*~__bXGZrb7_mcgm)0EtuECQD_qw0=ru2@|TC%*gYDDZ|L=W0A<*+*~;6`oalSF@sQn zy<4CQuV}qWH%lSj@@8X-Z1JX+v4j_D2Fse|d#ajKW?Y`Xagipkng%#s9zXCv^>n#N z7mAQ|*0^uRFJ+xIQfAymvtHZnCTTafk;X#1aL~!{y|HL6AC0SOT8X7)Eo#WR5ly5t zRgq`(d^Qoz<*jQ;*_b)=og%fguCY3L;OG%h1=IZt!F^QI2PhaI0cD0GmwN6^>;{k01oaJaj`Zt5rr+whKqiF-^2`{%@+Mvbm)z<5hA z8vFA;S=o}vhi$UfqOH<@tpJR{cNfPqY9ej|fHafa>rZ#~la!oX{Qga-%NK9}gf#%m4rY literal 0 HcmV?d00001 diff --git a/test/chrome_sync.py b/test/chrome_sync.py new file mode 100644 index 0000000..19a16bd --- /dev/null +++ b/test/chrome_sync.py @@ -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"] diff --git a/test/t3.py b/test/t3.py new file mode 100644 index 0000000..4239a02 --- /dev/null +++ b/test/t3.py @@ -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)) diff --git a/test/t4.py b/test/t4.py new file mode 100644 index 0000000..695102d --- /dev/null +++ b/test/t4.py @@ -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() diff --git a/test/t5.py b/test/t5.py new file mode 100644 index 0000000..3cb11f6 --- /dev/null +++ b/test/t5.py @@ -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()) diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..274d9e4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,142 @@ + + + + + 북마크 추가 앱 (Eel 버전) + + + + +

북마크 추가 앱 (Eel 버전)

+ +
+ + + +
+
+ + + + + + + + +