commit 562c88aa8ea9edf00ca943cb7cf485085bb0d68a Author: Envy_PC Date: Mon Jan 6 16:41:43 2025 +0900 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96c2e27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +Lib/ +libs/ +Scripts/ +Include/ +build/ +*.cfg +*.log \ No newline at end of file diff --git a/bookmaker.ico b/bookmaker.ico new file mode 100644 index 0000000..a80158c Binary files /dev/null and b/bookmaker.ico differ diff --git a/main.py b/main.py new file mode 100644 index 0000000..de3e8af --- /dev/null +++ b/main.py @@ -0,0 +1,566 @@ +import sys +import os +import sqlite3 +import subprocess +import json +from datetime import datetime, time +import logging +import traceback + +import pandas as pd +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, + QComboBox, QSpinBox, QCheckBox, QProgressBar, QTextEdit, QFileDialog, QWidget, QMessageBox, QDialog, QTableWidget, QTableWidgetItem, QAbstractItemView, QDialogButtonBox +) +from PySide6.QtCore import Qt, QThread, Signal, QSettings + +class BookmarkWorker(QThread): + progress = Signal(int) + log = Signal(str) + completed = Signal() # 작업 완료 시 호출 + + def __init__(self, bookmarks, folder_name, bookmarks_path, chrome_path, remove_existing): + super().__init__() + self.bookmarks = bookmarks + self.folder_name = folder_name + self.bookmarks_path = bookmarks_path + self.chrome_path = chrome_path + self.remove_existing = remove_existing + + def run(self): + try: + # JSON 파일 읽기 + if not os.path.exists(self.bookmarks_path): + self.log.emit(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.emit("JSON 파일이 비어 있어 기본값으로 초기화합니다.") + else: + bookmarks_data = json.loads(file_content) + except json.JSONDecodeError as e: + self.log.emit(f"JSON 파일 파싱 중 오류 발생: {str(e)}") + bookmarks_data = {"roots": {"bookmark_bar": {"children": []}}} + self.log.emit("JSON 파일을 기본값으로 초기화합니다.") + + # 기존 북마크 제거 + if self.remove_existing: + bookmarks_data["roots"]["bookmark_bar"] = self.remove_existing_bookmarks( + bookmarks_data["roots"]["bookmark_bar"] + ) + + # 북마크 추가 + total_bookmarks = 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, chunk_start in enumerate(range(0, total_bookmarks, chunk_size), start=1): + # 하위 폴더 이름 생성 + folder_name = f"거상북마크-{self.folder_name}-{idx}" # 하위 폴더 이름 (예: 거상북마크-중국-1) + + sub_folder = { + "type": "folder", + "name": folder_name, + "children": [] + } + + # 하위 폴더에 북마크 추가 + for bookmark in self.bookmarks[chunk_start:chunk_start + chunk_size]: + sub_folder["children"].append({ + "type": "url", + "name": bookmark['name'], # 몰 이름을 북마크 이름으로 설정 + "url": bookmark['url'] + }) + + # 상위 폴더에 하위 폴더 추가 + parent_folder["children"].append(sub_folder) + + # 진행률 업데이트 + progress = int((chunk_start + len(self.bookmarks[chunk_start:chunk_start + chunk_size])) / total_bookmarks * 100) + self.progress.emit(progress) + + # 즐겨찾기 바에 상위 폴더 추가 + bookmark_bar["children"].append(parent_folder) + + # 수정된 JSON 파일 저장 + with open(self.bookmarks_path, "w", encoding="utf-8") as file: + json.dump(bookmarks_data, file, indent=4, ensure_ascii=False) + + self.log.emit("즐겨찾기 추가 작업이 완료되었습니다!") + + # 작업 완료 시 크롬 실행 + if os.path.exists(self.chrome_path): + subprocess.Popen([self.chrome_path, "chrome://bookmarks/"]) + self.log.emit("크롬 북마크 관리 페이지를 열었습니다.") + else: + self.log.emit(f"크롬 실행 파일을 찾을 수 없습니다: {self.chrome_path}") + + self.completed.emit() + + except Exception as e: + self.log.emit(f"오류 발생: {str(e)}", exc_info=True) + self.progress.emit(0) + + def remove_existing_bookmarks(self, node): + """ + 북마크 데이터를 재귀적으로 탐색하여 '거상북마크'로 시작하는 모든 폴더를 제거합니다. + :param node: 북마크 데이터의 현재 노드 + :return: 필터링된 북마크 데이터 + """ + if not isinstance(node, dict): + return node + + # 폴더 이름이 '거상북마크'로 시작하면 제거 + if node.get("type") == "folder" and node.get("name", "").startswith("거상북마크"): + self.log.emit(f"제거된 폴더: {node.get('name')}") + return None + + # 자식(children)이 있는 경우 재귀적으로 탐색 + 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 + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + logging.basicConfig(level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s") + self.logger = logging.getLogger(__name__) + + # datetime을 문자열로 변환하는 어댑터 등록 + sqlite3.register_adapter(datetime, lambda dt: dt.strftime("%Y-%m-%d %H:%M:%S")) + + # 문자열을 datetime 객체로 변환하는 컨버터 등록 + sqlite3.register_converter("DATETIME", lambda s: datetime.strptime(s.decode("utf-8"), "%Y-%m-%d %H:%M:%S")) + + # 고정된 비밀번호 + self.stored_password = "365" + + # QSettings 초기화 (비밀번호 저장 및 불러오기용) + self.settings = QSettings("WhenRideMycar", "BookmarkAdder") + + # 비밀번호 확인 창 표시 + self.password = None + self.show_password_dialog() + + # 프로그램 실행 가능 여부 확인 + if self.password != self.stored_password: + QMessageBox.critical(self, "비밀번호 오류", "비밀번호가 일치하지 않습니다. 프로그램을 종료합니다.") + sys.exit() + + self.setWindowTitle("크롬 즐겨찾기 추가 프로그램 (by 내차는언제타냐 feat.거상110+님)") + self.setGeometry(300, 300, 800, 500) + + # UI 구성 + self.layout = QVBoxLayout() + self.buttons_layout = QHBoxLayout() + self.filter_layout = QHBoxLayout() + + # DB 입력 버튼 + self.db_input_button = QPushButton("DB 입력") + self.db_input_button.setToolTip("엑셀 파일을 선택하여 DB에 저장합니다. 기존 DB를 제거하거나 추가로 데이터를 입력할 수 있습니다.") + self.db_input_button.clicked.connect(self.load_excel) + + # 기존 DB 제거 체크박스 + self.remove_db_checkbox = QCheckBox("기존 DB 제거") + self.remove_db_checkbox.setToolTip("체크하면 기존 DB를 삭제하고 새롭게 데이터를 입력합니다.") + + # 데이터 보기 버튼 + self.view_data_button = QPushButton("데이터 보기") + self.view_data_button.setToolTip("DB 내용을 테이블 형식으로 표시하고 데이터를 수정할 수 있습니다.") + self.view_data_button.setEnabled(False) + self.view_data_button.clicked.connect(self.view_data) + + # 드롭다운: 국가 + self.country_label = QLabel("국가") + self.country_dropdown = QComboBox() + self.country_dropdown.addItems(["미국", "유럽", "중국", "일본", "한국", "기타", "랜덤"]) + self.country_dropdown.setCurrentText("중국") + + # 드롭다운: 등급 + self.grade_label = QLabel("등급") + self.grade_dropdown = QComboBox() + self.grade_dropdown.addItems(["일반", "파워", "빅파워", "랜덤"]) + self.grade_dropdown.setCurrentText("랜덤") + + # 스핀박스: 갯수 + self.count_label = QLabel("갯수") + self.count_spinbox = QSpinBox() + self.count_spinbox.setMinimum(100) + self.count_spinbox.setMaximum(1000) + self.count_spinbox.setSingleStep(100) + self.count_spinbox.setValue(100) + + # 기존 북마크 제거 체크박스 + self.remove_existing_checkbox = QCheckBox("기존 북마크 제거") + self.remove_existing_checkbox.setToolTip("체크하면 '거상북마크'로 시작하는 모든 북마크를 제거합니다.") + + # 실행 버튼 + self.run_button = QPushButton("실행") + self.run_button.setToolTip("선택된 필터 조건에 따라 DB에서 데이터를 가져와 북마크를 추가합니다.") + self.run_button.clicked.connect(self.run_task) + + # 로그 박스 + self.log_box = QTextEdit() + self.log_box.setReadOnly(True) + + # 프로그레스 바 + self.progress_bar = QProgressBar() + + # 필터 레이아웃 구성 + self.filter_layout.addWidget(self.country_label) + self.filter_layout.addWidget(self.country_dropdown) + self.filter_layout.addWidget(QLabel(" ")) + self.filter_layout.addWidget(self.grade_label) + self.filter_layout.addWidget(self.grade_dropdown) + self.filter_layout.addWidget(QLabel(" ")) + self.filter_layout.addWidget(self.count_label) + self.filter_layout.addWidget(self.count_spinbox) + self.filter_layout.addWidget(QLabel(" ")) + self.filter_layout.addWidget(self.remove_existing_checkbox) + + # 버튼 레이아웃 구성 + self.buttons_layout.addWidget(self.db_input_button) + self.buttons_layout.addWidget(self.remove_db_checkbox) + self.buttons_layout.addWidget(self.view_data_button) + self.buttons_layout.addWidget(self.run_button) + + # 메인 레이아웃 구성 + self.layout.addLayout(self.filter_layout) + self.layout.addLayout(self.buttons_layout) + self.layout.addWidget(self.log_box) + self.layout.addWidget(self.progress_bar) + + # 메인 위젯 설정 + self.main_widget = QWidget() + self.main_widget.setLayout(self.layout) + self.setCentralWidget(self.main_widget) + + # 상태 변수 + self.db_path = "markets.db" + + # DB 파일 확인 + self.check_db() + + def show_password_dialog(self): + """비밀번호 입력 창을 표시""" + dialog = QDialog(self) + dialog.setWindowTitle("비밀번호 입력") + dialog.setFixedSize(300, 150) + + layout = QVBoxLayout(dialog) + + # 비밀번호 입력 필드 + password_label = QLabel("비밀번호:") + password_input = QLineEdit() + password_input.setEchoMode(QLineEdit.Password) + + # 저장된 비밀번호 불러오기 + saved_password = self.settings.value("password", "") + if saved_password: + password_input.setText(saved_password) + + # 비밀번호 저장 체크박스 + save_password_checkbox = QCheckBox("비밀번호 저장") + save_password_checkbox.setChecked(bool(saved_password)) # 저장된 비밀번호가 있으면 체크 + + # 확인 버튼 + button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel, parent=dialog) + button_box.accepted.connect(dialog.accept) + button_box.rejected.connect(dialog.reject) + + # 레이아웃 구성 + layout.addWidget(password_label) + layout.addWidget(password_input) + layout.addWidget(save_password_checkbox) + layout.addWidget(button_box) + + # 다이얼로그 실행 + if dialog.exec() == QDialog.Accepted: + self.password = password_input.text() + + # 비밀번호 저장 처리 + if save_password_checkbox.isChecked(): + self.settings.setValue("password", self.password) + else: + self.settings.remove("password") + else: + self.password = None + + def log(self, message, exc_info=False): + """ + 메시지를 로그에 출력하고, GUI 로그 박스에도 표시 + :param message: 출력할 메시지 + :param exc_info: True일 경우 traceback 정보 포함 + """ + if exc_info: + # traceback 정보 포함하여 로깅 + self.logger.error(message, exc_info=True) + error_traceback = traceback.format_exc() + self.log_box.append(f"{message}\n{error_traceback}") + else: + self.logger.info(message) + self.log_box.append(message) + + def check_db(self): + if not os.path.exists(self.db_path): + QMessageBox.warning(self, "DB 없음", "DB 파일이 없습니다. 엑셀을 지정하여 DB를 생성해주세요.") + else: + self.log("DB 파일이 로드되었습니다.") + self.view_data_button.setEnabled(True) + + def load_excel(self): + file_path, _ = QFileDialog.getOpenFileName(self, "엑셀 파일 선택", "", "Excel Files (*.xlsx *.xls)") + if not file_path: + return + + try: + self.log("엑셀 파일을 불러오는 중...") + + # 파일 확장자 확인 + ext = os.path.splitext(file_path)[-1].lower() + + # 필수 열 이름 정의 + required_columns = ['country', 'mall_grade', 'mall_name', 'mall_url'] + + # 엑셀 파일 읽기 + if ext == ".xls": + try: + import xlrd + df = pd.read_excel(file_path, sheet_name=0, engine="xlrd") + except ImportError: + self.log("xlrd 라이브러리가 필요합니다. 'pip install xlrd>=2.0.1' 명령으로 설치하세요.") + return + except xlrd.biffh.XLRDError as e: + self.log(f"엑셀 파일을 열 수 없습니다. 파일 형식을 확인하세요: {e}") + return + elif ext == ".xlsx": + try: + df = pd.read_excel(file_path, sheet_name=0, engine="openpyxl") + except Exception as e: + self.log(f"엑셀 파일을 열 수 없습니다: {e}", exc_info=True) + return + else: + self.log("지원되지 않는 파일 형식입니다. .xls 또는 .xlsx 파일을 선택하세요.") + return + + # 엑셀에서 가져온 열과 필수 열 비교 + available_columns = [col for col in required_columns if col in df.columns] + if not available_columns: + self.log("엑셀 파일에 필수 열이 없습니다. 필요한 열: " + ", ".join(required_columns)) + return + + # 필요한 열만 가져오기 + df = df[available_columns] + + # 누락된 열은 빈 값으로 추가 + for col in required_columns: + if col not in df.columns: + df[col] = "" # 누락된 열은 빈 값으로 채움 + + # 열 이름을 정렬하여 설정 + df = df[required_columns] + + # datetime.time 타입 데이터를 문자열로 변환 + def convert_time_to_string(x): + try: + if isinstance(x, time): # 시간이면 변환 + return x.strftime("%H:%M:%S") + return x # 아니면 그대로 반환 + except Exception as e: + self.log(f"데이터 변환 중 에러 발생: {str(e)}") + return x + + if 'mall_name' in df.columns: + df['mall_name'] = df['mall_name'].apply(convert_time_to_string) + if 'mall_url' in df.columns: + df['mall_url'] = df['mall_url'].apply(convert_time_to_string) + + conn = sqlite3.connect(self.db_path) + + if self.remove_db_checkbox.isChecked(): + # 기존 DB 제거 및 새 테이블 생성 + 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 + ) + """) + self.log("기존 DB가 제거되었습니다. 새롭게 생성되었습니다.") + + # 데이터 저장, 중복 방지 + df.drop_duplicates(subset=['country', 'mall_grade', 'mall_name', 'mall_url'], inplace=True) + + # id 열을 자동 생성하지 않으므로, 기존 테이블 스키마를 유지하면서 데이터를 삽입 + 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() + self.log("DB 저장 완료") + except Exception as e: + self.log(f"엑셀 파일을 불러오는 중 에러 발생: {str(e)}", exc_info=True) + + def view_data(self): + try: + conn = sqlite3.connect(self.db_path) + query = "SELECT * FROM markets" + df = pd.read_sql_query(query, conn) + conn.close() + + dialog = QDialog(self) + dialog.setWindowTitle("DB 데이터 보기") + dialog.setGeometry(100, 100, 800, 400) + + table = QTableWidget(dialog) + table.setRowCount(len(df)) + table.setColumnCount(len(df.columns)) + table.setHorizontalHeaderLabels(df.columns) + + for i, row in df.iterrows(): + for j, value in enumerate(row): + item = QTableWidgetItem(str(value)) + if j == 0: # id 열 (편집 불가능) + item.setFlags(item.flags() & ~Qt.ItemIsEditable) + table.setItem(i, j, item) + + # 네 번째 열 너비 조정 + table.setColumnWidth(4, table.columnWidth(3) * 2) + + # 테이블 정렬 활성화 + table.setSortingEnabled(True) + + # 정렬을 위한 헤더 클릭 이벤트 연결 + table.horizontalHeader().sectionClicked.connect(lambda index: self.sort_table(table, index)) + + table.cellChanged.connect(lambda: self.confirm_edit(table)) + table.resize(780, 380) + dialog.exec() + except Exception as e: + QMessageBox.critical(self, "오류", f"DB 데이터를 불러오는 중 오류 발생: {e}", exc_info=True) + + + def sort_table(self, table, column_index): + """ + 테이블 데이터를 정렬하는 함수 + :param table: QTableWidget + :param column_index: 정렬할 열 인덱스 + """ + try: + order = table.horizontalHeader().sortIndicatorOrder() # 현재 정렬 방향 확인 + table.sortItems(column_index, order) # 해당 열에 따라 정렬 + self.log(f"{column_index + 1}번째 열을 정렬했습니다. 방향: {'오름차순' if order == Qt.AscendingOrder else '내림차순'}") + except Exception as e: + self.log(f"테이블 정렬 중 오류 발생: {str(e)}", exc_info=True) + + def confirm_edit(self, table): + reply = QMessageBox.question(self, "확인", "수정된 데이터를 저장하시겠습니까?", QMessageBox.Yes | QMessageBox.No) + if reply == QMessageBox.Yes: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + for row in range(table.rowCount()): + # id는 수정하지 않으므로 첫 번째 열은 그대로 사용 + record_id = table.item(row, 0).text() + country = table.item(row, 1).text() + mall_grade = table.item(row, 2).text() + mall_name = table.item(row, 3).text() + mall_url = table.item(row, 4).text() + + # id를 기준으로 업데이트 + cursor.execute(""" + UPDATE markets + SET country = ?, mall_grade = ?, mall_name = ?, mall_url = ? + WHERE id = ? + """, (country, mall_grade, mall_name, mall_url, record_id)) + + conn.commit() + conn.close() + self.log("수정된 데이터가 저장되었습니다.") + else: + self.log("수정이 취소되었습니다.") + + def run_task(self): + country = self.country_dropdown.currentText() + grade = self.grade_dropdown.currentText() + count = self.count_spinbox.value() + remove_existing = self.remove_existing_checkbox.isChecked() + + conn = sqlite3.connect(self.db_path) + query = "SELECT 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}'" + + # 랜덤 정렬 추가 + query += " ORDER BY RANDOM()" + # 갯수 제한 추가 + query += f" LIMIT {count}" + + try: + df = pd.read_sql_query(query, conn) + conn.close() + + self.bookmarks = df.to_dict("records") + self.log(f"{len(self.bookmarks)}개의 북마크를 추출했습니다.") + except Exception as e: + self.log(f"DB 쿼리 실행 중 오류 발생: {str(e)}", exc_info=True) + conn.close() + return + + folder_name = f"거상북마크-{grade}" + self.bookmarks_path = os.path.expanduser(r"~\AppData\Local\Google\Chrome\User Data\Default\Bookmarks") + self.chrome_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe" + + self.worker = BookmarkWorker(self.bookmarks, folder_name, self.bookmarks_path, self.chrome_path, remove_existing) + self.worker.progress.connect(self.progress_bar.setValue) + self.worker.log.connect(self.log) + self.worker.completed.connect(self.task_completed) + self.worker.start() + + def task_completed(self): + self.log("작업이 완료되었습니다!") + QMessageBox.information(self, "완료", "즐겨찾기 추가 작업이 완료되었습니다.") + + + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/markets.db b/markets.db new file mode 100644 index 0000000..2bf9596 Binary files /dev/null and b/markets.db differ diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f6a5dbc --- /dev/null +++ b/setup.py @@ -0,0 +1,46 @@ +from cx_Freeze import setup, Executable +import os +import sys + +# 애플리케이션 이름과 버전 +application_name = "크롬 즐겨찾기 추가 프로그램" +application_version = "1.0.0" + +# 실행 파일 생성 설정 +base = None +if sys.platform == "win32": + base = "Win32GUI" # 콘솔 창 없이 실행하려면 'Win32GUI' 설정 + +# 애플리케이션 메인 파일 설정 +main_file = "main.py" + +# 필요한 추가 파일 설정 (예: 리소스 파일, 아이콘 등) +include_files = [ + ("markets.db", "markets.db"), # 데이터베이스 파일 (필요 시) +] + +# 빌드 옵션 +build_options = { + "packages": ["os", "sys", "sqlite3", "subprocess", "json", "pandas", "datetime", "PySide6"], + "include_files": include_files, + "excludes": [], # tkinter 미사용 시 제외 +} + +# 실행 파일 설정 +executables = [ + Executable( + script=main_file, + base=base, + target_name="BookmarkAdder.exe", + icon="bookmaker.ico" + ) +] + +# setup() 함수 호출 +setup( + name=application_name, + version=application_version, + description="크롬 즐겨찾기 추가 프로그램 (내차는언제타냐 feat.110+)", + options={"build_exe": build_options}, + executables=executables, +)