import customtkinter as ctk from tkinter import filedialog from view.theme import theme_manager from pathlib import Path 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("", self._on_enter) self.widget.bind("", self._on_leave) self.widget.bind("", 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 = " by KH.CHOI" self.title("VOC 모니터링 설정" + self.author) 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() 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()