Initial commit

This commit is contained in:
Envy_PC 2025-01-06 16:41:43 +09:00
commit 562c88aa8e
5 changed files with 619 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
Lib/
libs/
Scripts/
Include/
build/
*.cfg
*.log

BIN
bookmaker.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

566
main.py Normal file
View File

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

BIN
markets.db Normal file

Binary file not shown.

46
setup.py Normal file
View File

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