BookMakerAdder/main.py

1067 lines
46 KiB
Python

import sqlite3
import subprocess
import json
from datetime import datetime, time
from src.version import __version__
import xlrd
from PySide6.QtGui import QIcon
import logging
import traceback
import os
import sys
import winreg
import psutil
import subprocess
import pygetwindow as gw
import glob
import pandas as pd
from PySide6.QtWidgets import (
QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QLineEdit, QButtonGroup, QRadioButton,
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, browser_path, selected_browser, remove_existing):
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.chunk_size = 1000
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 = self.chunk_size # 하위 폴더에 넣을 북마크 수
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("즐겨찾기 추가 작업이 완료되었습니다!")
# 작업 완료 시 브라우저 실행
self.run_browser_with_profile(self.browser_path, self.bookmarks_path, browser_name=self.selected_browser)
# 작업 완료 후 '카피맨' 실행 및 창 활성화
self.run_and_focus_copyman()
self.completed.emit()
except Exception as e:
self.log.emit(f"오류 발생: {str(e)}", exc_info=True)
self.progress.emit(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.emit("웨일 브라우저의 기존 프로세스를 종료했습니다.")
except Exception as e:
self.log.emit(f"웨일 브라우저 프로세스 종료 중 오류 발생: {str(e)}")
if os.path.exists(browser_path):
# 북마크 경로에서 프로필 이름 추출
profile_directory = os.path.basename(os.path.dirname(bookmarks_path))
# 접속 URL 설정
if "크롬" in browser_name.lower():
bookmark_page = "chrome://bookmarks/"
elif "웨일" in browser_name.lower():
bookmark_page = "whale://bookmarks/"
else:
bookmark_page = "about:blank" # 기본값 (알 수 없는 브라우저)
if profile_directory: # 유효한 프로필 디렉토리가 있는 경우
subprocess.Popen([
browser_path,
f"--profile-directory={profile_directory}",
bookmark_page
])
self.log.emit(f"{browser_name} 북마크 관리 페이지를 '{profile_directory}' 프로필로 열었습니다.")
else:
# 프로필 경로를 찾을 수 없는 경우 기본 실행
subprocess.Popen([browser_path, bookmark_page])
self.log.emit(f"{browser_name} 북마크 관리 페이지를 기본 프로필로 열었습니다.")
else:
self.log.emit(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.emit(f"{program_name} 실행 중 (PID: {pid})")
# 실행 중인 프로그램 창으로 전환
if not self.focus_window_by_title(window_title_start):
self.log.emit(f"'{window_title_start}'로 시작하는 창을 찾을 수 없습니다.")
else:
self.log.emit(f"{program_name} 실행 중이 아님. 실행 시도 중...")
# 바로가기를 찾아 실행
shortcut_path = self.find_shortcut_in_start_menu(shortcut_name)
if shortcut_path:
self.run_program(shortcut_path)
else:
self.log.emit(f"'{shortcut_name}' 바로가기를 찾을 수 없습니다.")
except Exception as e:
error_traceback = traceback.format_exc()
self.log.emit(f"카피맨 실행 중 오류 발생: {str(e)}\n{error_traceback}")
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):
"""창 제목이 특정 문자열로 시작하는 창을 찾아 활성화"""
for window in gw.getAllWindows():
if window.title and window.title.startswith(title_start): # 창 제목이 조건에 맞는 경우
try:
window.activate()
self.log.emit(f"프로그램 창으로 전환: {window.title}")
return True
except Exception as e:
self.log.emit(f"창 활성화 실패: {e}")
return False
return False
def find_shortcut_in_start_menu(self, shortcut_name):
"""시작 메뉴에서 바로가기 찾기"""
user_start_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")
for start_menu_path in [user_start_menu, all_users_start_menu]:
shortcut_path = glob.glob(os.path.join(start_menu_path, f"**\\{shortcut_name}.lnk"), recursive=True)
if shortcut_path:
return shortcut_path[0]
return None
def run_program(self, shortcut_path):
"""프로그램 실행"""
try:
subprocess.Popen([shortcut_path], shell=True)
self.log.emit(f"프로그램 실행: {shortcut_path}")
except Exception as e:
self.log.emit(f"프로그램 실행 실패: {e}")
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(f"크롬 즐겨찾기 추가 프로그램 (by 내차는언제타냐 feat.거상110+님) - v{__version__}")
self.setGeometry(300, 300, 800, 500)
# UI 구성
self.layout = QVBoxLayout()
self.buttons_layout = QHBoxLayout()
self.filter_layout = QHBoxLayout()
self.browser_selection_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.sample_excel_button = QPushButton("추가양식 보기")
self.sample_excel_button.setToolTip("추가 양식을 확인합니다.")
self.sample_excel_button.clicked.connect(self.load_sample_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.chrome_path_button = QPushButton("크롬 경로 설정")
self.chrome_path_button.clicked.connect(self.set_chrome_path)
self.whale_path_button = QPushButton("웨일 경로 설정")
self.whale_path_button.clicked.connect(self.set_whale_path)
# 브라우저 선택 라디오버튼
self.chrome_radio = QRadioButton("크롬")
self.whale_radio = QRadioButton("웨일")
self.whale_radio.clicked.connect(self.whale_radio_clicked)
self.browser_group = QButtonGroup()
self.browser_group.addButton(self.chrome_radio)
self.browser_group.addButton(self.whale_radio)
self.chrome_radio.setChecked(True) # 기본값: 크롬
# 드롭다운: 국가
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(1000)
self.count_spinbox.setMaximum(50000)
self.count_spinbox.setSingleStep(1000)
self.count_spinbox.setValue(1000)
# 스핀박스: 폴더당 북마크 갯수
self.fcount_label = QLabel("폴더당 북마크 갯수")
self.fcount_spinbox = QSpinBox()
self.fcount_spinbox.setMinimum(1000)
self.fcount_spinbox.setMaximum(50000)
self.fcount_spinbox.setSingleStep(100)
self.fcount_spinbox.setValue(100)
self.fcount_spinbox.valueChanged.connect(self.update_chunk_size)
# 기존 북마크 제거 체크박스
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.extract_based_checkbox = QCheckBox("추출 횟수 기반 추출")
self.extract_based_checkbox.setChecked(False) # 기본값 OFF
self.extract_based_checkbox.setToolTip("체크 시 추출 횟수가 낮은 데이터부터 추출합니다.")
self.extract_based_checkbox.stateChanged.connect(self.toggle_extract_options)
# 최대 추출 횟수 스핀박스
self.max_extract_label = QLabel("최대 추출 횟수")
self.max_extract_spinbox = QSpinBox()
self.max_extract_spinbox.setMinimum(1)
self.max_extract_spinbox.setMaximum(5)
self.max_extract_spinbox.setValue(1) # 기본값
# 리셋 버튼
self.reset_button = QPushButton("추출 횟수 초기화")
self.reset_button.setToolTip("DB의 모든 추출 횟수를 0으로 초기화합니다.")
self.reset_button.clicked.connect(self.reset_extract_count)
# 로그 박스
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.fcount_label)
self.filter_layout.addWidget(self.fcount_spinbox)
self.filter_layout.addWidget(QLabel(" "))
self.filter_layout.addWidget(self.remove_existing_checkbox)
# # 버튼 레이아웃 구성
# self.buttons_layout.addWidget(self.chrome_path_button)
# self.buttons_layout.addWidget(self.whale_path_button)
# 브라우저 선택 레이아웃 구성
self.browser_selection_layout.addWidget(QLabel("브라우저 선택:"))
self.browser_selection_layout.addWidget(self.chrome_radio)
self.browser_selection_layout.addWidget(self.whale_radio)
self.browser_selection_layout.addWidget(self.chrome_path_button)
self.browser_selection_layout.addWidget(self.whale_path_button)
self.buttons_layout.addWidget(self.db_input_button)
self.buttons_layout.addWidget(self.sample_excel_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.filter_layout.addWidget(self.extract_based_checkbox)
self.filter_layout.addWidget(self.max_extract_label)
self.filter_layout.addWidget(self.max_extract_spinbox)
self.filter_layout.addWidget(self.reset_button)
# 메인 레이아웃 구성
self.layout.addLayout(self.filter_layout)
self.layout.addLayout(self.buttons_layout)
self.layout.addLayout(self.browser_selection_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.setStyleSheet("""
QMainWindow { background-color: #f9f9f9; }
QPushButton { background-color: #4CAF50; color: white; padding: 6px 12px; border-radius: 4px; }
QPushButton:hover { background-color: #45a049; }
QLineEdit, QComboBox, QSpinBox, QCheckBox, QTextEdit { font-size: 14px; }
QLabel { font-size: 14px; }
QProgressBar { height: 20px; }
""")
# 상태 변수
self.db_path = "markets.db"
# DB 파일 확인
self.check_db()
# 브라우저 선택 복원
self.restore_browser_selection()
# 브라우저 선택 상태 변경 시 QSettings에 저장
self.chrome_radio.toggled.connect(self.save_browser_selection)
self.whale_radio.toggled.connect(self.save_browser_selection)
# 크롬/웨일 경로 초기화
self.chrome_path = self.get_browser_path("chrome") or "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"
self.chrome_bookmarks_path = os.path.join(
self.get_specific_profile_path("chrome"), "Bookmarks"
) if self.get_specific_profile_path("chrome") else ""
self.whale_path = self.get_browser_path("whale") or "C:\\Program Files\\Naver\\Naver Whale\\Application\\whale.exe"
self.whale_bookmarks_path = os.path.join(
self.get_specific_profile_path("whale"), "Bookmarks"
) if self.get_specific_profile_path("whale") else ""
self.log(f"크롬 실행 파일 경로가 설정되었습니다: {self.chrome_path}")
self.log(f"웨일 실행 파일 경로가 설정되었습니다: {self.whale_path}")
self.log(f"크롬 프로파일 경로가 설정되었습니다: {self.chrome_bookmarks_path}")
self.log(f"웨일 프로파일 경로가 설정되었습니다: {self.whale_bookmarks_path}")
# 아이콘 설정 (추가된 부분)
self.set_app_icon()
def update_chunk_size(self):
"""폴더당 북마크 갯수 변경 시 chunk_size 업데이트"""
# self.log(f"폴더당 북마크 갯수가 변경되었습니다: {self.worker.chunk_size}")
def set_app_icon(self):
"""애플리케이션 아이콘을 설정합니다."""
base_dir = self.get_base_dir()
icon_path = os.path.join(base_dir, "bookmaker.ico")
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
else:
pass
# 브라우저 실행 파일 경로 가져오기
def get_browser_path(self, browser_name):
try:
reg_path = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, f"{reg_path}\\{browser_name}.exe") as key:
return winreg.QueryValue(key, None)
except FileNotFoundError:
return None
# 브라우저 프로파일 경로 가져오기
def get_browser_profile_path(self, browser_name):
if browser_name == "chrome":
return os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\User Data")
elif browser_name == "whale":
return os.path.expandvars(r"%LOCALAPPDATA%\Naver\Naver Whale\User Data")
return None
# 기본 프로파일 경로 가져오기
def get_specific_profile_path(self, browser_name, profile_name="Default"):
"""
주어진 브라우저 이름에 따라 프로필 경로를 찾고 Bookmarks 파일이 존재하는 폴더를 반환합니다.
:param browser_name: 브라우저 이름 ("chrome" 또는 "whale")
:param profile_name: 기본 프로필 이름 (기본값은 "Default")
:return: Bookmarks 파일이 존재하는 프로필 경로 또는 None
"""
base_profile_path = self.get_browser_profile_path(browser_name)
if not base_profile_path:
return None
# 확인 순서: Default -> Profile 1 -> Profile 2 -> ...
profile_names = [profile_name] + [f"Profile {i}" for i in range(1, 10)] # 최대 10개의 프로필 확인
for profile in profile_names:
specific_profile_path = os.path.join(base_profile_path, profile)
bookmarks_file = os.path.join(specific_profile_path, "Bookmarks")
if os.path.exists(bookmarks_file):
self.log(f"'{profile}' 프로필에서 Bookmarks 파일을 찾았습니다: {bookmarks_file}")
return specific_profile_path
# Bookmarks 파일이 없는 경우 None 반환
self.log(f"'{browser_name}' 브라우저에서 유효한 Bookmarks 파일을 찾을 수 없습니다.")
return None
def set_chrome_path(self):
"""크롬 실행 파일 경로 설정"""
file_path, _ = QFileDialog.getOpenFileName(self, "크롬 실행 파일 선택", "", "Executable Files (*.exe)")
if file_path:
self.chrome_path = file_path
self.log(f"크롬 실행 파일 경로가 설정되었습니다: {file_path}")
def set_whale_path(self):
"""웨일 실행 파일 경로 설정"""
file_path, _ = QFileDialog.getOpenFileName(self, "웨일 실행 파일 선택", "", "Executable Files (*.exe)")
if file_path:
self.whale_path = file_path
self.log(f"웨일 실행 파일 경로가 설정되었습니다: {file_path}")
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 save_browser_selection(self):
"""사용자가 선택한 브라우저를 QSettings에 저장"""
if self.chrome_radio.isChecked():
self.settings.setValue("selected_browser", "chrome")
self.log("사용자가 크롬 브라우저를 선택했습니다.")
elif self.whale_radio.isChecked():
self.settings.setValue("selected_browser", "whale")
self.log("사용자가 웨일 브라우저를 선택했습니다.")
def restore_browser_selection(self):
"""QSettings에서 저장된 브라우저 선택 상태를 복원"""
selected_browser = self.settings.value("selected_browser", "chrome") # 기본값은 크롬
if selected_browser == "chrome":
self.chrome_radio.setChecked(True)
elif selected_browser == "whale":
self.whale_radio.setChecked(True)
self.log(f"저장된 브라우저 선택을 복원했습니다: {selected_browser}")
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 get_base_dir(self):
"""
실행 환경에 따라 base_dir을 설정하는 메서드.
cx_Freeze로 패키징된 경우 실행 파일의 경로, 일반 Python 환경일 경우 __file__을 기준으로 설정.
"""
if getattr(sys, 'frozen', False): # 패키징된 경우
base_dir = os.path.dirname(sys.executable)
internal_dir = os.path.join(base_dir, 'lib', 'src') # lib 디렉토리 포함
if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정
return internal_dir
else: # 일반 Python 실행 환경
base_dir = os.path.dirname(os.path.abspath(__file__))
debug_dir = os.path.join(base_dir, 'src') # lib 디렉토리 포함
return debug_dir
def load_sample_excel(self):
"""'추가양식.xlsx' 파일을 엽니다."""
try:
base_dir = self.get_base_dir()
file_path = os.path.join(base_dir, "추가양식.xlsx")
if os.path.exists(file_path):
os.startfile(file_path)
self.log(f"추가 양식 파일 열기: {file_path}")
else:
self.log(f"추가 양식 파일을 찾을 수 없습니다: {file_path}")
QMessageBox.warning(self, "파일 없음", "추가 양식 파일을 찾을 수 없습니다.")
except Exception as e:
self.log(f"추가 양식 파일 열기 중 오류 발생: {str(e)}", exc_info=True)
QMessageBox.critical(self, "오류", f"추가 양식 파일을 열 수 없습니다: {str(e)}")
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()
# 열 이름 매핑 정의
column_name_mapping = {
'mall_url': ['mall_url(필수)', 'mall_url', '마켓링크', 'URL'],
'mall_name': ['mall_name', '셀러명', '판매자명'],
'mall_grade': ['mall_grade', '등급', '판매자등급'],
'country': ['country', '국가', '지역']
}
required_columns = ['mall_url']
all_expected_columns = list(column_name_mapping.keys())
# 엑셀 파일 읽기
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
# 엑셀 열 이름과 정의된 이름 매핑
rename_dict = {}
excel_columns = df.columns.tolist()
for internal_name, possible_names in column_name_mapping.items():
for excel_col in excel_columns:
if excel_col in possible_names:
rename_dict[excel_col] = internal_name
break # 하나라도 매칭되면 다음 내부 이름으로 이동
# 데이터프레임 열 이름 변경
df.rename(columns=rename_dict, inplace=True)
# 필수 열이 있는지 확인
if 'mall_url' not in df.columns:
self.log(f"엑셀 파일에 필수 열({column_name_mapping['mall_url']}) 중 하나라도 있어야 합니다.")
return
# 필요한 열만 선택 (매핑된 내부 이름 사용)
available_columns = [col for col in all_expected_columns if col in df.columns]
df = df[available_columns].copy()
# 누락된 열은 빈 값으로 채우기
for col in all_expected_columns:
if col not in df.columns:
df[col] = ""
# 열 순서 설정 (데이터베이스 테이블 순서와 맞춤)
df = df[['country', 'mall_grade', 'mall_name', 'mall_url']]
# 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)
cursor = conn.cursor()
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,
extract_count INTEGER DEFAULT 0
)
""")
self.log("기존 DB가 제거되었습니다. 새롭게 생성되었습니다.")
# 기존 DB에 `extract_count` 필드 추가
try:
conn.execute("ALTER TABLE markets ADD COLUMN extract_count INTEGER DEFAULT 0")
except sqlite3.OperationalError:
# 컬럼이 이미 있는 경우 무시
pass
added_count = 0
skipped_count = 0
# 데이터 저장 및 중복 방지
for _, row in df.iterrows():
mall_url = row['mall_url']
cursor.execute("SELECT mall_url FROM markets WHERE mall_url=?", (mall_url,))
existing_mall = cursor.fetchone()
if not existing_mall:
cursor.execute("""
INSERT INTO markets (country, mall_grade, mall_name, mall_url)
VALUES (?, ?, ?, ?)
""", (row['country'], row['mall_grade'], row['mall_name'], mall_url))
self.log(f"새로운 몰 추가: {mall_url}")
added_count += 1
else:
self.log(f"중복된 몰 건너뛰기: {mall_url}")
skipped_count += 1
conn.commit()
conn.close()
self.log("DB 저장 완료")
self.show_load_excel_result_message(added_count, skipped_count)
except Exception as e:
self.log(f"엑셀 파일을 불러오는 중 에러 발생: {str(e)}", exc_info=True)
def show_load_excel_result_message(self, added_count, skipped_count):
"""엑셀 로드 완료 후 결과를 메시지 박스로 보여줍니다."""
QMessageBox.information(
self,
"엑셀 처리 완료",
f"{added_count}개의 몰이 추가되었고, {skipped_count}개의 중복된 몰은 제외되었습니다."
)
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 whale_radio_clicked(self):
# 메세지 창 띄우기
QMessageBox.warning(
self,
"주의!!",
"웨일의 경우 현재 실행된 웨일브라우저를 강제종료합니다."
)
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 ensure_extract_count_column(self, conn):
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(markets)")
columns = [row[1] for row in cursor.fetchall()]
if "extract_count" not in columns:
conn.execute("ALTER TABLE markets ADD COLUMN extract_count INTEGER DEFAULT 0")
def toggle_extract_options(self):
if self.extract_based_checkbox.isChecked():
self.max_extract_spinbox.setEnabled(True)
else:
self.max_extract_spinbox.setEnabled(False)
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()
extract_based = self.extract_based_checkbox.isChecked()
max_extract = self.max_extract_spinbox.value()
conn = sqlite3.connect(self.db_path)
try:
# 추출 횟수 필드 확인 및 추가
self.ensure_extract_count_column(conn)
# 기본 쿼리
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 < ?"
query += " ORDER BY extract_count ASC, RANDOM()"
else:
query += " ORDER BY RANDOM()"
query += f" LIMIT {count}"
# 추출
if extract_based:
df = pd.read_sql_query(query, conn, params=(max_extract,))
else:
df = pd.read_sql_query(query, conn)
if df.empty:
# 추출 가능한 데이터가 없는 경우 리셋
if extract_based:
reply = QMessageBox.question(
self,
"데이터 초기화",
"추출 가능한 데이터가 없습니다. 모든 추출 횟수를 초기화하시겠습니까?",
QMessageBox.Yes | QMessageBox.No
)
if reply == QMessageBox.Yes:
self.reset_extract_count()
else:
return
else:
QMessageBox.warning(self, "추출 실패", "DB에 추출 가능한 데이터가 없습니다.")
return
self.bookmarks = df.to_dict("records")
# 추출 횟수 업데이트
for record in df.to_dict("records"):
conn.execute("UPDATE markets SET extract_count = extract_count + 1 WHERE id = ?", (record["id"],))
conn.commit()
self.log(f"{len(self.bookmarks)}개의 북마크를 추출했습니다.")
except Exception as e:
self.log(f"DB 쿼리 실행 중 오류 발생: {str(e)}", exc_info=True)
conn.close()
return
conn.close()
folder_name = f"거상북마크-{grade}"
# 사용자가 선택한 브라우저에 따라 경로 설정
if self.chrome_radio.isChecked():
selected_bookmarks_path = self.chrome_bookmarks_path
selected_browser_path = self.chrome_path
selected_browser = '크롬'
self.log("크롬 브라우저가 선택되었습니다.")
elif self.whale_radio.isChecked():
selected_bookmarks_path = self.whale_bookmarks_path
selected_browser_path = self.whale_path
selected_browser = '웨일'
self.log("웨일 브라우저가 선택되었습니다.")
else:
QMessageBox.warning(self, "브라우저 선택 오류", "브라우저를 선택해주세요.")
return
# 선택된 브라우저 경로 유효성 확인
if not os.path.exists(selected_browser_path):
QMessageBox.warning(self, "브라우저 경로 오류", "선택된 브라우저 실행 파일 경로가 유효하지 않습니다. 경로를 설정해주세요.")
return
if not os.path.exists(selected_bookmarks_path):
QMessageBox.warning(self, "북마크 경로 오류", "선택된 브라우저의 북마크 경로가 유효하지 않습니다. 경로를 설정해주세요.")
return
self.worker = BookmarkWorker(self.bookmarks, folder_name, selected_bookmarks_path, selected_browser_path, selected_browser, remove_existing)
self.worker.chunk_size = self.fcount_spinbox.value()
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 reset_extract_count(self):
conn = sqlite3.connect(self.db_path)
try:
# `extract_count` 필드가 없으면 추가
try:
conn.execute("ALTER TABLE markets ADD COLUMN extract_count INTEGER DEFAULT 0")
except sqlite3.OperationalError:
# 이미 필드가 존재하는 경우 무시
pass
# 모든 추출 횟수를 0으로 초기화
conn.execute("UPDATE markets SET extract_count = 0")
conn.commit()
self.log("모든 추출 횟수가 초기화되었습니다.")
QMessageBox.information(self, "초기화 완료", "모든 추출 횟수가 초기화되었습니다.")
except Exception as e:
self.log(f"추출 횟수 초기화 중 오류 발생: {str(e)}", exc_info=True)
finally:
conn.close()
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()