VOC_Monitor/app/view/dialogs/settings_dialog.py

367 lines
15 KiB
Python

import customtkinter as ctk
from tkinter import filedialog
from view.theme import theme_manager
from pathlib import Path
from updater.__version__ import VERSION, APP_NAME
class HoverTooltip:
"""마우스 오버 시 표시되는 간단한 툴팁"""
def __init__(self, widget, text: str, delay_ms: int = 300):
self.widget = widget
self.text = text
self.delay_ms = delay_ms
self._after_id = None
self._tooltip = None
self.widget.bind("<Enter>", self._on_enter)
self.widget.bind("<Leave>", self._on_leave)
self.widget.bind("<ButtonPress>", self._on_leave)
def _on_enter(self, _event=None):
self._schedule_show()
def _on_leave(self, _event=None):
self._cancel_show()
self._hide()
def _schedule_show(self):
self._cancel_show()
self._after_id = self.widget.after(self.delay_ms, self._show)
def _cancel_show(self):
if self._after_id is not None:
self.widget.after_cancel(self._after_id)
self._after_id = None
def _show(self):
if self._tooltip is not None:
return
x = self.widget.winfo_pointerx() + 12
y = self.widget.winfo_pointery() + 12
self._tooltip = ctk.CTkToplevel(self.widget)
self._tooltip.overrideredirect(True)
self._tooltip.attributes("-topmost", True)
self._tooltip.geometry(f"+{x}+{y}")
label = ctk.CTkLabel(
self._tooltip,
text=self.text,
justify="left",
wraplength=320,
corner_radius=8,
fg_color=("#f2f2f2", "#2b2b2b"),
text_color=("#111111", "#f0f0f0"),
padx=10,
pady=8,
font=theme_manager.get_font(11),
)
label.pack()
def _hide(self):
if self._tooltip is not None:
self._tooltip.destroy()
self._tooltip = None
class SettingsDialog(ctk.CTkToplevel):
def __init__(self, controller, parent=None):
super().__init__(master=parent)
self.controller = controller
self.author = "KH.Choi"
self.title(f"{APP_NAME} 설정")
self.geometry("500x500")
# parent가 있으면 transient 설정 (Z-order 문제 해결)
if parent:
self.transient(parent)
self.lift()
self.focus_force()
# 1. Tabview 생성
self.tabview = ctk.CTkTabview(self)
self.tabview.pack(padx=20, pady=20, fill="both", expand=True)
self.tab_login = self.tabview.add("로그인")
self.tab_noti = self.tabview.add("알림 설정")
self.tab_crawl = self.tabview.add("수집 설정") # 기존 크롤링 설정 통합 가능
self.tab_theme = self.tabview.add("테마")
self.tab_system = self.tabview.add("시스템")
self._init_login_tab()
self._init_crawl_tab()
self._init_theme_tab()
self._init_system_tab()
self._init_notification_tab()
self.meta_label = ctk.CTkLabel(
self,
text=f"버전 v{VERSION} | 제작 {self.author}",
font=theme_manager.get_font(11),
text_color=("gray45", "gray65")
)
self.meta_label.pack(pady=(0, 10))
def _init_login_tab(self):
font = theme_manager.get_font(12)
ctk.CTkLabel(self.tab_login, text="사번 (ID)", font=font).pack(pady=5)
self.entry_id = ctk.CTkEntry(self.tab_login)
self.entry_id.pack(pady=5)
self.entry_id.insert(0, self.controller.settings['login']['id'])
ctk.CTkLabel(self.tab_login, text="비밀번호", font=font).pack(pady=5)
self.entry_pw = ctk.CTkEntry(self.tab_login, show="*")
self.entry_pw.pack(pady=5)
self.entry_pw.insert(0, self.controller.settings['login']['pw'])
ctk.CTkButton(self.tab_login, text="설정 저장", command=self.save_all).pack(pady=20)
def _init_crawl_tab(self):
font = theme_manager.get_font(12)
ctk.CTkLabel(self.tab_crawl, text="수집 주기 (분)", font=font).pack(pady=5)
self.combo_interval = ctk.CTkComboBox(self.tab_crawl, values=["3", "5", "10", "15", "30"])
self.combo_interval.pack(pady=5)
self.combo_interval.set(str(self.controller.settings['crawling']['interval_minutes']))
ctk.CTkLabel(self.tab_crawl, text="신규 알림 주기 (분)", font=font).pack(pady=5)
self.combo_noti_interval = ctk.CTkComboBox(self.tab_crawl, values=["1", "3", "5", "10", "15", "30"])
self.combo_noti_interval.pack(pady=5)
self.combo_noti_interval.set(
str(self.controller.settings.get('noti', {}).get('db_check_interval_minutes', 5))
)
ctk.CTkLabel(self.tab_crawl, text="미확인 알림 주기 (분)", font=font).pack(pady=5)
self.combo_unchecked_interval = ctk.CTkComboBox(self.tab_crawl, values=["5", "10", "15", "30", "60"])
self.combo_unchecked_interval.pack(pady=5)
self.combo_unchecked_interval.set(
str(self.controller.settings.get('noti', {}).get('unchecked_check_interval_minutes', 30))
)
self.unchecked_delay_var = ctk.BooleanVar(
value=self.controller.settings.get('noti', {}).get('unchecked_delay_enabled', True)
)
unchecked_delay_frame = ctk.CTkFrame(self.tab_crawl, fg_color="transparent")
unchecked_delay_frame.pack(pady=8, anchor="w", padx=20)
self.check_unchecked_delay = ctk.CTkSwitch(
unchecked_delay_frame,
text="미확인 글 30분 경과 후에만 알림",
variable=self.unchecked_delay_var
)
self.check_unchecked_delay.pack(side="left")
self.unchecked_delay_help = ctk.CTkLabel(
unchecked_delay_frame,
text="?",
width=20,
height=20,
corner_radius=10,
fg_color=("#d9d9d9", "#3a3a3a"),
text_color=("#222222", "#f0f0f0"),
font=theme_manager.get_font(11, "bold"),
cursor="hand2"
)
self.unchecked_delay_help.pack(side="left", padx=(8, 0))
self.unchecked_delay_tooltip = HoverTooltip(
self.unchecked_delay_help,
"ON: 생성 후 30분이 지난 미확인 글만 알림합니다.\n"
"OFF: 생성 시간과 무관하게 미확인 글 전체를 알림합니다."
)
ctk.CTkLabel(self.tab_crawl, text="관심 키워드 (콤마로 구분)", font=font).pack(pady=5)
self.entry_crawl_kw = ctk.CTkEntry(self.tab_crawl, width=300)
self.entry_crawl_kw.pack(pady=5)
self.entry_crawl_kw.insert(0, ",".join(self.controller.settings['crawling']['keywords']))
ctk.CTkLabel(self.tab_crawl, text="관심 부서 (콤마로 구분)", font=font).pack(pady=5)
self.entry_dept = ctk.CTkEntry(self.tab_crawl, width=300)
self.entry_dept.pack(pady=5)
self.entry_dept.insert(0, ",".join(self.controller.settings['crawling']['target_depts']))
ctk.CTkButton(self.tab_crawl, text="설정 저장", command=self.save_all).pack(pady=20)
def _init_theme_tab(self):
font = theme_manager.get_font(12)
ctk.CTkLabel(self.tab_theme, text="UI 테마 모드", font=font).pack(pady=10)
self.combo_theme = ctk.CTkComboBox(self.tab_theme, values=["System", "Dark", "Light"])
self.combo_theme.pack(pady=5)
current_theme = self.controller.settings.get('theme', 'System')
self.combo_theme.set(current_theme)
ctk.CTkButton(self.tab_theme, text="테마 적용 및 저장", command=self.save_all).pack(pady=20)
def _init_system_tab(self):
font_bold = theme_manager.get_font(13, "bold")
# === 로그 관리 섹션 ===
ctk.CTkLabel(self.tab_system, text="시스템 로그 관리", font=font_bold).pack(pady=(10, 5), anchor="w", padx=20)
def open_logs():
from view.dialogs.log_viewer_dialog import LogViewerDialog
LogViewerDialog(self)
ctk.CTkButton(
self.tab_system,
text="📋 로그 확인",
command=open_logs,
width=200,
height=35
).pack(pady=5, padx=20, anchor="w")
# 구분선
ctk.CTkLabel(self.tab_system, text="", height=1, fg_color="gray").pack(fill="x", padx=20, pady=15)
# === 보고서 저장 경로 섹션 ===
ctk.CTkLabel(self.tab_system, text="보고서 저장 경로", font=font_bold).pack(pady=(10, 5), anchor="w", padx=20)
# 현재 경로 표시 프레임
path_frame = ctk.CTkFrame(self.tab_system, fg_color="transparent")
path_frame.pack(fill="x", padx=20, pady=5)
# 현재 경로 가져오기 (기본값: output 폴더)
current_path = self.controller.settings.get('report', {}).get('output_path', '')
if not current_path:
# 기본 경로 설정
from utils.path_utils import get_base_dir
current_path = str(get_base_dir().parent / "output")
self.report_path_label = ctk.CTkLabel(
path_frame,
text=current_path,
font=theme_manager.get_font(11),
text_color=("gray40", "gray60"),
anchor="w"
)
self.report_path_label.pack(side="left", fill="x", expand=True, padx=(0, 10))
# 경로 변경 버튼
ctk.CTkButton(
path_frame,
text="📁 폴더 선택",
command=self._select_report_folder,
width=120,
height=35,
fg_color=("#1f538d", "#2980b9"),
hover_color=("#174a7a", "#1f6fa8")
).pack(side="left")
# 안내 문구
ctk.CTkLabel(
self.tab_system,
text="※ HWP 및 PDF 보고서가 저장될 폴더를 지정합니다.",
font=theme_manager.get_font(10),
text_color="gray"
).pack(anchor="w", padx=45, pady=(5, 0))
ctk.CTkButton(self.tab_system, text="설정 저장", command=self.save_all).pack(pady=20)
def _select_report_folder(self):
"""보고서 저장 폴더 선택 다이얼로그"""
current_path = self.report_path_label.cget("text")
# 폴더 선택 다이얼로그
selected_folder = filedialog.askdirectory(
title="보고서 저장 폴더 선택",
initialdir=current_path if Path(current_path).exists() else None
)
if selected_folder:
self.report_path_label.configure(text=selected_folder)
# 즉시 저장 (사용자 편의)
if 'report' not in self.controller.settings:
self.controller.settings['report'] = {}
self.controller.settings['report']['output_path'] = selected_folder
self.controller.save_settings()
def _init_notification_tab(self):
# 1. 알림 소리 설정
self.sound_var = ctk.BooleanVar(value=self.controller.settings.get('noti', {}).get('sound', True))
self.check_sound = ctk.CTkSwitch(self.tab_noti, text="알림 발생 시 소리 재생", variable=self.sound_var)
self.check_sound.pack(pady=15, anchor="w", padx=20)
ctk.CTkLabel(self.tab_noti, text="", height=1, fg_color="gray").pack(fill="x", padx=20, pady=10)
# 2. 관심 조건 필터링 토글
self.use_related_filter_var = ctk.BooleanVar(
value=self.controller.settings.get('noti', {}).get('use_related_filter', True)
)
related_filter_frame = ctk.CTkFrame(self.tab_noti, fg_color="transparent")
related_filter_frame.pack(pady=10, anchor="w", padx=20)
self.sw_kw = ctk.CTkSwitch(
related_filter_frame,
text="관심 조건(키워드/부서) 일치 글만 알림",
variable=self.use_related_filter_var
)
self.sw_kw.pack(side="left")
self.related_filter_help = ctk.CTkLabel(
related_filter_frame,
text="?",
width=20,
height=20,
corner_radius=10,
fg_color=("#d9d9d9", "#3a3a3a"),
text_color=("#222222", "#f0f0f0"),
font=theme_manager.get_font(11, "bold"),
cursor="hand2"
)
self.related_filter_help.pack(side="left", padx=(8, 0))
self.related_filter_tooltip = HoverTooltip(
self.related_filter_help,
"ON: 수집 설정의 키워드/관심부서 조건(OR)을 만족한 글만 신규/미확인 알림합니다.\n"
"OFF: 조건과 무관하게 신규/미확인 전체 글을 알림합니다."
)
# 가이드 문구
self.lbl_guide = ctk.CTkLabel(
self.tab_noti,
text=(
"※ ON: 수집 설정의 키워드/관심부서 조건(OR)을 만족한 글만 알림\n"
"※ OFF: 조건과 무관하게 신규/미확인 전체 글에 대해 알림"
),
font=theme_manager.get_font(11),
text_color="gray"
)
self.lbl_guide.pack(anchor="w", padx=45)
ctk.CTkButton(self.tab_noti, text="설정 저장", command=self.save_all).pack(pady=20)
def save_all(self):
self.controller.settings['login']['id'] = self.entry_id.get()
self.controller.settings['login']['pw'] = self.entry_pw.get()
self.controller.settings['crawling']['interval_minutes'] = int(self.combo_interval.get())
self.controller.settings['crawling']['keywords'] = [k.strip() for k in self.entry_crawl_kw.get().split(',') if k.strip()]
self.controller.settings['crawling']['target_depts'] = [k.strip() for k in self.entry_dept.get().split(',') if k.strip()]
if 'noti' not in self.controller.settings:
self.controller.settings['noti'] = {}
self.controller.settings['noti']['db_check_interval_minutes'] = int(self.combo_noti_interval.get())
self.controller.settings['noti']['unchecked_check_interval_minutes'] = int(self.combo_unchecked_interval.get())
self.controller.settings['noti']['unchecked_delay_enabled'] = self.unchecked_delay_var.get()
# 테마 저장 및 적용
selected_theme = self.combo_theme.get()
self.controller.settings['theme'] = selected_theme
theme_manager.set_theme(selected_theme)
self.controller.settings['noti']['sound'] = self.sound_var.get()
self.controller.settings['noti']['use_related_filter'] = self.use_related_filter_var.get()
# 과거 알림 키워드 설정 정리 (현재는 수집 설정 키워드만 사용)
self.controller.settings['noti'].pop('use_keywords', None)
self.controller.settings['noti'].pop('keywords', None)
# 보고서 경로 저장 (이미 _select_report_folder에서 저장되지만 일관성 유지)
if 'report' not in self.controller.settings:
self.controller.settings['report'] = {}
self.controller.settings['report']['output_path'] = self.report_path_label.cget("text")
self.controller.save_settings()
self.controller.update_config_runtime()
self.destroy()