• Y
  • List All
  • Feedback
    • This Project
    • All Projects
Profile Account settings Log out
  • Favorite
  • Project
  • All
Loading...
  • Log in
  • Sign up
hylee / salarySplitSend star
  • Project homeH
  • CodeC
  • IssueI
  • Pull requestP
  • Review R
  • MilestoneM
  • BoardB
  • Files
  • Commit
  • Branches
salarySplitSendpayslip_splitter_gui_v3.py
Download as .zip file
File name
Commit message
Commit date
__pycache__
first commit
08-28
src
first commit
08-28
test
first commit
08-28
README.md
first commit
08-28
build.bat
first commit
08-28
email_config.json
first commit
08-28
employees.json
first commit
08-28
payslip_splitter_gui_v3.py
first commit
08-28
requirements.txt
first commit
08-28
급여명세서분할기_메일기능_v4.exe
.exe file add
08-28
hehihoho3@gmail.com 08-28 722a0e6 first commit UNIX
Raw Open in browser Change history
import tkinter as tk from tkinter import filedialog, messagebox, ttk, scrolledtext import fitz # PyMuPDF import re import os import threading from tkinter import font from tkinterdnd2 import DND_FILES, TkinterDnD import json from datetime import datetime # 사용자 정의 모듈 임포트 from src.employee_manager import EmployeeManager from src.email_sender import PayslipEmailSender, EmailConfig from src.pdf_preview import PDFPreviewManager def extract_birth_date_numbers(text): """ 텍스트에서 '생년월일' 라벨 오른쪽에 있는 날짜를 찾아 숫자만 반환합니다. """ patterns = [ r'생년월일\s*[::]?\s*(\d{4})[-.\/\s](\d{1,2})[-.\/\s](\d{1,2})', r'생년월일\s+(\d{4})[-.\/](\d{1,2})[-.\/](\d{1,2})', ] for pattern in patterns: match = re.search(pattern, text) if match: year, month, day = match.groups() month = month.zfill(2) day = day.zfill(2) return year + month + day return None class PayslipFileItem: """개별 급여명세서 파일 아이템 UI 컴포넌트""" def __init__(self, parent_frame, employee_name, email, pdf_path, preview_manager, on_status_change=None): self.parent_frame = parent_frame self.employee_name = employee_name self.email = email self.pdf_path = pdf_path self.preview_manager = preview_manager self.on_status_change = on_status_change # PDF 정보 로드 self.pdf_info = preview_manager.get_pdf_info(pdf_path) if pdf_path and os.path.exists(pdf_path) else {} self.create_ui() def create_ui(self): # 메인 컨테이너 프레임 self.main_frame = ttk.Frame(self.parent_frame, relief='ridge', padding=5) self.main_frame.pack(fill='x', padx=5, pady=2) # 상단: 직원 정보 및 체크박스 top_frame = ttk.Frame(self.main_frame) top_frame.pack(fill='x') # 발송 여부 체크박스 self.send_var = tk.BooleanVar(value=bool(self.email)) self.send_check = ttk.Checkbutton(top_frame, variable=self.send_var, command=self.on_send_status_change) self.send_check.pack(side='left', padx=(0, 5)) # 직원 정보 email_display = self.email if self.email else "❌ 매칭되지 않음" info_color = 'black' if self.email else 'red' info_label = ttk.Label(top_frame, text=f"👤 {self.employee_name} → 📧 {email_display}", font=('맑은 고딕', 9, 'bold')) info_label.pack(side='left') # 중단: 파일 정보 및 버튼들 middle_frame = ttk.Frame(self.main_frame) middle_frame.pack(fill='x', pady=(5, 0)) # 파일 정보 if self.pdf_path and os.path.exists(self.pdf_path): file_size = self.preview_manager.format_file_size(self.pdf_info.get('file_size', 0)) page_count = self.pdf_info.get('page_count', 0) file_info_text = f"📄 {os.path.basename(self.pdf_path)} ({file_size})" if self.pdf_info.get('is_encrypted'): file_info_text += " 🔒 비밀번호 보호" else: file_info_text += f" ({page_count}페이지)" else: file_info_text = "📄 파일을 찾을 수 없습니다" file_label = ttk.Label(middle_frame, text=file_info_text) file_label.pack(side='left') # 버튼들 button_frame = ttk.Frame(middle_frame) button_frame.pack(side='right') if self.pdf_path and os.path.exists(self.pdf_path): ttk.Button(button_frame, text="📂 열기", command=self.open_file, width=8).pack(side='left', padx=2) ttk.Button(button_frame, text="📁 폴더", command=self.open_folder, width=8).pack(side='left', padx=2) # 발송 완료 상태 표시용 self.sent_status = False # 발송 완료 상태 self.sent_label = None # 발송 완료 라벨 def mark_as_sent(self): """발송 완료로 표시""" self.sent_status = True if not self.sent_label: self.sent_label = ttk.Label(self.main_frame, text="✅ 발송 완료", foreground='green', font=('맑은 고딕', 9, 'bold')) self.sent_label.pack(side='right', padx=5) # 체크박스 비활성화 self.send_check.config(state='disabled') def open_folder(self): """파일이 있는 폴더 열기""" if self.pdf_path and os.path.exists(self.pdf_path): import subprocess import platform try: folder_path = os.path.dirname(self.pdf_path) if platform.system() == 'Windows': subprocess.run(['explorer', '/select,', self.pdf_path]) elif platform.system() == 'Darwin': # macOS subprocess.run(['open', '-R', self.pdf_path]) else: # Linux subprocess.run(['xdg-open', folder_path]) except Exception as e: messagebox.showerror("오류", f"폴더를 열 수 없습니다:\n{str(e)}") else: messagebox.showwarning("파일 없음", "폴더를 열 파일이 없습니다.") def open_file(self): """시스템 기본 프로그램으로 파일 열기""" if self.pdf_path and os.path.exists(self.pdf_path): # 비밀번호 보호된 파일이면 안내 메시지 표시 if self.pdf_info.get('is_encrypted'): response = messagebox.askyesno( "비밀번호 보호된 파일", f"{self.employee_name}님의 급여명세서는 비밀번호로 \n보호되어 있습니다.\n\n🔑 비밀번호: 생년월일 8자리\n(예: 1990년 1월 15일 → 19900115)\n\n파일을 여시겠습니까?" ) if not response: return success = self.preview_manager.open_file_with_system(self.pdf_path) if not success: messagebox.showerror("오류", "파일을 열 수 없습니다.") else: messagebox.showwarning("파일 없음", "열 파일이 없습니다.") def on_send_status_change(self): """발송 상태 변경시 UI 업데이트""" if self.on_status_change: self.on_status_change() def get_send_data(self): """이메일 발송 데이터 반환""" # 발송 완료된 항목은 제외 if self.sent_status: print(f"[DEBUG] 발송 완료된 항목 제외: {self.employee_name}") return None if self.send_var.get() and self.email and self.pdf_path: if os.path.exists(self.pdf_path): file_size = os.path.getsize(self.pdf_path) print(f"[DEBUG] 발송 대상: {self.employee_name} -> {self.email}") print(f"[DEBUG] PDF 파일: {self.pdf_path} ({file_size} bytes)") return { "name": self.employee_name, "email": self.email, "pdf_path": self.pdf_path } else: print(f"[ERROR] PDF 파일 없음: {self.pdf_path}") return None class EmployeeManagementTab: """직원 관리 탭""" def __init__(self, parent_notebook, employee_manager): self.employee_manager = employee_manager self.selected_employee = None # 직원 관리 탭 생성 self.frame = ttk.Frame(parent_notebook) parent_notebook.add(self.frame, text="직원 관리") self.setup_ui() self.refresh_employee_list() def setup_ui(self): # 메인 컨테이너 main_container = ttk.Frame(self.frame, padding=10) main_container.pack(fill='both', expand=True) # 상단: 검색 영역 search_frame = ttk.LabelFrame(main_container, text="직원 검색", padding=5) search_frame.pack(fill='x', pady=(0, 10)) self.search_var = tk.StringVar() ttk.Label(search_frame, text="검색:").pack(side='left') search_entry = ttk.Entry(search_frame, textvariable=self.search_var, width=20) search_entry.pack(side='left', padx=5) search_entry.bind('<KeyRelease>', self.on_search) ttk.Button(search_frame, text="전체보기", command=self.show_all).pack(side='left', padx=5) # 중앙: 좌우 분할 영역 paned_window = ttk.PanedWindow(main_container, orient='horizontal') paned_window.pack(fill='both', expand=True) # 왼쪽: 직원 목록 left_frame = ttk.LabelFrame(paned_window, text="직원 목록", padding=5) paned_window.add(left_frame, weight=2) # 직원 목록 (TreeView 사용) columns = ('name', 'email') self.tree = ttk.Treeview(left_frame, columns=columns, show='headings', height=15) self.tree.heading('name', text='이름') self.tree.heading('email', text='이메일') self.tree.column('name', width=100) self.tree.column('email', width=200) # 스크롤바 scrollbar = ttk.Scrollbar(left_frame, orient='vertical', command=self.tree.yview) self.tree.configure(yscrollcommand=scrollbar.set) self.tree.pack(side='left', fill='both', expand=True) scrollbar.pack(side='right', fill='y') # 트리 선택 이벤트 self.tree.bind('<<TreeviewSelect>>', self.on_tree_select) # 목록 하단 버튼들 list_button_frame = ttk.Frame(left_frame) list_button_frame.pack(fill='x', pady=5) ttk.Button(list_button_frame, text="선택삭제", command=self.delete_selected).pack(side='left', padx=2) # 오른쪽: 편집 영역 right_frame = ttk.LabelFrame(paned_window, text="직원 정보 편집", padding=5) paned_window.add(right_frame, weight=1) # 편집 폼 form_frame = ttk.Frame(right_frame) form_frame.pack(fill='x', pady=5) ttk.Label(form_frame, text="이름:").grid(row=0, column=0, sticky='w', pady=2) self.name_var = tk.StringVar() self.name_entry = ttk.Entry(form_frame, textvariable=self.name_var, width=20) self.name_entry.grid(row=0, column=1, padx=5, pady=2) ttk.Label(form_frame, text="이메일:").grid(row=1, column=0, sticky='w', pady=2) self.email_var = tk.StringVar() self.email_entry = ttk.Entry(form_frame, textvariable=self.email_var, width=30) self.email_entry.grid(row=1, column=1, padx=5, pady=2) # 편집 버튼들 edit_button_frame = ttk.Frame(right_frame) edit_button_frame.pack(fill='x', pady=10) ttk.Button(edit_button_frame, text="새로 추가", command=self.add_employee).pack(fill='x', pady=2) ttk.Button(edit_button_frame, text="수정", command=self.update_employee).pack(fill='x', pady=2) ttk.Button(edit_button_frame, text="삭제", command=self.delete_employee).pack(fill='x', pady=2) # 직원 관리 옵션 option_frame = ttk.LabelFrame(right_frame, text="관리 옵션", padding=5) option_frame.pack(fill='x', pady=10) ttk.Button(option_frame, text="전체 선택", command=self.select_all_employees).pack(fill='x', pady=1) ttk.Button(option_frame, text="선택 해제", command=self.deselect_all_employees).pack(fill='x', pady=1) # 하단: 빠른 추가 quick_frame = ttk.LabelFrame(main_container, text="빠른 추가", padding=5) quick_frame.pack(fill='x', pady=10) quick_container = ttk.Frame(quick_frame) quick_container.pack() ttk.Label(quick_container, text="이름:").pack(side='left') self.quick_name_var = tk.StringVar() quick_name_entry = ttk.Entry(quick_container, textvariable=self.quick_name_var, width=15) quick_name_entry.pack(side='left', padx=5) ttk.Label(quick_container, text="이메일:").pack(side='left', padx=(10, 0)) self.quick_email_var = tk.StringVar() quick_email_entry = ttk.Entry(quick_container, textvariable=self.quick_email_var, width=25) quick_email_entry.pack(side='left', padx=5) ttk.Button(quick_container, text="추가", command=self.quick_add_employee).pack(side='left', padx=5) # 엔터키 바인딩 quick_name_entry.bind('<Return>', lambda e: self.quick_add_employee()) quick_email_entry.bind('<Return>', lambda e: self.quick_add_employee()) def refresh_employee_list(self, filter_text=""): """직원 목록 새로고침""" # 기존 항목 제거 for item in self.tree.get_children(): self.tree.delete(item) # 직원 목록 추가 if filter_text: employees = self.employee_manager.search_employees(filter_text) else: employees = self.employee_manager.get_all_employees() for name, email in sorted(employees.items()): self.tree.insert('', 'end', values=(name, email)) def on_search(self, event=None): """검색 필터링""" self.refresh_employee_list(self.search_var.get()) def show_all(self): """전체 목록 보기""" self.search_var.set("") self.refresh_employee_list() def on_tree_select(self, event): """트리에서 항목 선택시""" selection = self.tree.selection() if selection: item = self.tree.item(selection[0]) values = item['values'] self.name_var.set(values[0]) self.email_var.set(values[1]) self.selected_employee = values[0] def add_employee(self): """직원 추가""" name = self.name_var.get().strip() email = self.email_var.get().strip() if not name or not email: messagebox.showwarning("경고", "이름과 이메일을 모두 입력해주세요.") return if self.employee_manager.add_employee(name, email): messagebox.showinfo("성공", f"'{name}' 직원을 추가했습니다.") self.refresh_employee_list() self.clear_form() else: messagebox.showerror("오류", "이미 존재하는 직원입니다.") def update_employee(self): """직원 정보 수정""" if not self.selected_employee: messagebox.showwarning("경고", "수정할 직원을 선택해주세요.") return new_name = self.name_var.get().strip() new_email = self.email_var.get().strip() if not new_name or not new_email: messagebox.showwarning("경고", "이름과 이메일을 모두 입력해주세요.") return if self.employee_manager.update_employee(self.selected_employee, new_name, new_email): messagebox.showinfo("성공", f"'{new_name}' 직원 정보를 수정했습니다.") self.refresh_employee_list() self.selected_employee = new_name else: messagebox.showerror("오류", "직원 정보 수정에 실패했습니다.") def delete_employee(self): """직원 삭제""" if not self.selected_employee: messagebox.showwarning("경고", "삭제할 직원을 선택해주세요.") return result = messagebox.askyesno("확인", f"'{self.selected_employee}' 직원을 삭제하시겠습니까?") if result: if self.employee_manager.delete_employee(self.selected_employee): messagebox.showinfo("성공", f"'{self.selected_employee}' 직원을 삭제했습니다.") self.refresh_employee_list() self.clear_form() else: messagebox.showerror("오류", "직원 삭제에 실패했습니다.") def delete_selected(self): """선택된 직원들 삭제""" selected_items = self.tree.selection() if not selected_items: messagebox.showwarning("경고", "삭제할 직원을 선택해주세요.") return names = [self.tree.item(item)['values'][0] for item in selected_items] result = messagebox.askyesno("확인", f"{len(names)}명의 직원을 삭제하시겠습니까?") if result: success_count = 0 for name in names: if self.employee_manager.delete_employee(name): success_count += 1 messagebox.showinfo("완료", f"{success_count}명의 직원을 삭제했습니다.") self.refresh_employee_list() self.clear_form() def quick_add_employee(self): """빠른 추가""" name = self.quick_name_var.get().strip() email = self.quick_email_var.get().strip() if not name or not email: messagebox.showwarning("경고", "이름과 이메일을 모두 입력해주세요.") return if self.employee_manager.add_employee(name, email): messagebox.showinfo("성공", f"'{name}' 직원을 추가했습니다.") self.refresh_employee_list() self.quick_name_var.set("") self.quick_email_var.set("") else: messagebox.showerror("오류", "이미 존재하는 직원입니다.") def select_all_employees(self): """모든 직원 선택""" for item in self.employee_tree.get_children(): self.employee_tree.set(item, 'selected', True) self.employee_tree.selection_set(self.employee_tree.get_children()) def deselect_all_employees(self): """모든 직원 선택 해제""" for item in self.employee_tree.get_children(): self.employee_tree.set(item, 'selected', False) self.employee_tree.selection_set(()) def clear_form(self): """폼 초기화""" self.name_var.set("") self.email_var.set("") self.selected_employee = None class PayslipSplitterGUI: def __init__(self, root): self.root = root self.root.title("급여명세서 분할기 v3 - 이메일 자동발송 (드래그앤드롭 지원)") self.root.geometry("900x700") # 한글 폰트 설정 try: default_font = font.nametofont("TkDefaultFont") default_font.configure(family="맑은 고딕", size=9) self.root.option_add("*Font", default_font) except: pass # 관리자 초기화 self.employee_manager = EmployeeManager() self.email_sender = PayslipEmailSender() self.preview_manager = PDFPreviewManager() # 분할된 파일 정보 self.split_files = [] # [{"name": "이름", "pdf_path": "경로"}] self.file_items = [] # PayslipFileItem 객체들 # 드래그앤드롭 설정 self.root.drop_target_register(DND_FILES) self.root.dnd_bind('<<Drop>>', self.on_drop) self.setup_ui() def setup_ui(self): # 노트북 (탭) 컨테이너 self.notebook = ttk.Notebook(self.root) self.notebook.pack(fill='both', expand=True, padx=10, pady=10) # 탭 1: PDF 분할 self.setup_split_tab() # 탭 2: 이메일 발송 self.setup_email_tab() # 탭 3: 직원 관리 self.employee_tab = EmployeeManagementTab(self.notebook, self.employee_manager) def setup_split_tab(self): """PDF 분할 탭 설정""" split_frame = ttk.Frame(self.notebook) self.notebook.add(split_frame, text="PDF 분할") main_frame = ttk.Frame(split_frame, padding="10") main_frame.pack(fill='both', expand=True) # 파일 선택 섹션 file_frame = ttk.LabelFrame(main_frame, text="파일 선택", padding="5") file_frame.pack(fill='x', pady=(0, 10)) self.file_path_var = tk.StringVar() ttk.Label(file_frame, text="PDF 파일: (PDF 파일을 드래그해서 놓거나 찾아보기 버튼 사용)").pack(anchor='w') file_entry_frame = ttk.Frame(file_frame) file_entry_frame.pack(fill='x', pady=5) self.file_entry = ttk.Entry(file_entry_frame, textvariable=self.file_path_var) self.file_entry.pack(side='left', fill='x', expand=True, padx=(0, 5)) self.file_entry.drop_target_register(DND_FILES) self.file_entry.dnd_bind('<<Drop>>', self.on_drop) ttk.Button(file_entry_frame, text="찾아보기", command=self.browse_file).pack(side='right') # 옵션 섹션 options_frame = ttk.LabelFrame(main_frame, text="옵션", padding="5") options_frame.pack(fill='x', pady=(0, 10)) self.use_password_var = tk.BooleanVar(value=True) ttk.Checkbutton(options_frame, text="생년월일을 비밀번호로 설정", variable=self.use_password_var).pack(anchor='w', pady=2) self.overwrite_var = tk.BooleanVar(value=True) ttk.Checkbutton(options_frame, text="기존 파일 덮어쓰기", variable=self.overwrite_var).pack(anchor='w', pady=2) self.auto_email_var = tk.BooleanVar(value=False) ttk.Checkbutton(options_frame, text="분할 완료 후 이메일 자동 발송", variable=self.auto_email_var).pack(anchor='w', pady=2) # 처리 버튼 button_frame = ttk.Frame(main_frame) button_frame.pack(pady=(0, 10)) self.process_button = ttk.Button(button_frame, text="급여명세서 분할 시작", command=self.start_processing) self.process_button.pack() # 진행 상황 progress_frame = ttk.LabelFrame(main_frame, text="진행 상황", padding="5") progress_frame.pack(fill='x', pady=(0, 10)) self.progress_var = tk.StringVar(value="대기 중...") ttk.Label(progress_frame, textvariable=self.progress_var).pack(anchor='w') self.progress_bar = ttk.Progressbar(progress_frame, mode='indeterminate') self.progress_bar.pack(fill='x', pady=(5, 0)) # 결과 로그 log_frame = ttk.LabelFrame(main_frame, text="처리 결과", padding="5") log_frame.pack(fill='both', expand=True) self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state='disabled') self.log_text.pack(fill='both', expand=True) def setup_email_tab(self): """이메일 발송 탭 설정""" email_frame = ttk.Frame(self.notebook) self.notebook.add(email_frame, text="이메일 발송") main_frame = ttk.Frame(email_frame, padding="10") main_frame.pack(fill='both', expand=True) # 이메일 설정 섹션 config_frame = ttk.LabelFrame(main_frame, text="이메일 설정", padding="5") config_frame.pack(fill='x', pady=(0, 10)) config_grid = ttk.Frame(config_frame) config_grid.pack(fill='x') # 기본 이메일 설정 (수정 불가) ttk.Label(config_grid, text="사용자명:", font=('맑은 고딕', 9, 'bold')).grid(row=0, column=0, sticky='w', padx=(0, 5)) self.email_user_var = tk.StringVar(value="noreply@munjaon.co.kr") user_entry = ttk.Entry(config_grid, textvariable=self.email_user_var, width=30, state='readonly') user_entry.grid(row=0, column=1, padx=5) ttk.Label(config_grid, text="비밀번호:", font=('맑은 고딕', 9, 'bold')).grid(row=1, column=0, sticky='w', padx=(0, 5), pady=2) self.email_pass_var = tk.StringVar(value="iEWkihhyZipl") pass_entry = ttk.Entry(config_grid, textvariable=self.email_pass_var, width=30, show="*", state='readonly') pass_entry.grid(row=1, column=1, padx=5, pady=2) # 연결 테스트 버튼 test_button = ttk.Button(config_grid, text="🔌 연결 테스트", command=self.test_email_connection) test_button.grid(row=0, column=2, padx=10, rowspan=2) # 설정 안내 라벨 info_label = ttk.Label(config_grid, text="ℹ️ 기본 이메일 설정이 적용되어 있습니다.", font=('맑은 고딕', 8), foreground='gray') info_label.grid(row=2, column=0, columnspan=3, pady=(5, 0)) # 파일 매칭 섹션 self.files_frame = ttk.LabelFrame(main_frame, text="분할된 파일 및 발송 대상", padding="5") self.files_frame.pack(fill='both', expand=True, pady=(0, 10)) # 스크롤 가능한 프레임 self.files_canvas = tk.Canvas(self.files_frame, height=300) files_scrollbar = ttk.Scrollbar(self.files_frame, orient="vertical", command=self.files_canvas.yview) self.files_scrollable_frame = ttk.Frame(self.files_canvas) self.files_scrollable_frame.bind( "<Configure>", lambda e: self.files_canvas.configure(scrollregion=self.files_canvas.bbox("all")) ) self.files_canvas.create_window((0, 0), window=self.files_scrollable_frame, anchor="nw") self.files_canvas.configure(yscrollcommand=files_scrollbar.set) self.files_canvas.pack(side="left", fill="both", expand=True) files_scrollbar.pack(side="right", fill="y") # 발송 버튼 및 상태 send_frame = ttk.Frame(main_frame) send_frame.pack(fill='x', pady=(0, 10)) left_buttons = ttk.Frame(send_frame) left_buttons.pack(side='left') ttk.Button(left_buttons, text="전체 선택", command=self.select_all_files).pack(side='left', padx=(0, 5)) ttk.Button(left_buttons, text="전체 해제", command=self.deselect_all_files).pack(side='left', padx=5) ttk.Button(left_buttons, text="매칭 새로고침", command=self.refresh_file_matching).pack(side='left', padx=5) self.send_button = ttk.Button(send_frame, text="선택된 직원에게 이메일 발송", command=self.start_email_sending) self.send_button.pack(side='right') # 발송 상태 self.email_status_var = tk.StringVar(value="이메일 발송 대기") ttk.Label(main_frame, textvariable=self.email_status_var).pack() def on_drop(self, event): """드래그앤드롭으로 파일을 받았을 때 처리""" files = self.root.tk.splitlist(event.data) if files: file_path = files[0] if file_path.lower().endswith('.pdf'): self.file_path_var.set(file_path) self.log_message(f"파일이 드롭되었습니다: {os.path.basename(file_path)}") else: messagebox.showwarning("경고", "PDF 파일만 지원됩니다.") def browse_file(self): filename = filedialog.askopenfilename( title="급여명세서 PDF 파일 선택", filetypes=[("PDF files", "*.pdf"), ("All files", "*.*")] ) if filename: self.file_path_var.set(filename) def log_message(self, message): """로그 텍스트에 메시지 추가""" self.log_text.config(state='normal') self.log_text.insert(tk.END, message + '\n') self.log_text.config(state='disabled') self.log_text.see(tk.END) self.root.update() def clear_log(self): """로그 텍스트 클리어""" self.log_text.config(state='normal') self.log_text.delete(1.0, tk.END) self.log_text.config(state='disabled') def start_processing(self): """처리 시작 (별도 스레드에서)""" if not self.file_path_var.get(): messagebox.showerror("오류", "PDF 파일을 선택해주세요.") return if not os.path.exists(self.file_path_var.get()): messagebox.showerror("오류", "선택한 파일이 존재하지 않습니다.") return # UI 상태 변경 self.process_button.config(state='disabled') self.progress_bar.start() self.progress_var.set("처리 중...") self.clear_log() # 별도 스레드에서 처리 thread = threading.Thread(target=self.process_pdf) thread.daemon = True thread.start() def process_pdf(self): """PDF 처리 메인 로직""" try: input_pdf = self.file_path_var.get() use_password = self.use_password_var.get() overwrite = self.overwrite_var.get() self.log_message(f"파일 처리 시작: {os.path.basename(input_pdf)}") self.log_message(f"비밀번호 설정: {'사용' if use_password else '사용 안 함'}") self.log_message("=" * 50) base_name = os.path.splitext(os.path.basename(input_pdf))[0] doc = fitz.open(input_pdf) if not doc.page_count: self.log_message("오류: PDF에 페이지가 없습니다.") return self.log_message(f"총 {doc.page_count}페이지 처리를 시작합니다...") success_count = 0 fail_count = 0 self.split_files = [] for i, page in enumerate(doc): page_num = i + 1 text = page.get_text() name_match = re.search(r"사원명:\s*(\S+)", text) if name_match: name = name_match.group(1) output_filename = f"{base_name}_{name}.pdf" if os.path.exists(output_filename) and not overwrite: self.log_message(f"- {page_num}페이지: '{name}'님 파일이 이미 존재합니다. (건너뜀)") continue new_doc = fitz.open() new_doc.insert_pdf(doc, from_page=i, to_page=i) if use_password: birth_date_password = extract_birth_date_numbers(text) if birth_date_password: try: new_doc.save(output_filename, encryption=fitz.PDF_ENCRYPT_AES_256, user_pw=birth_date_password, owner_pw=birth_date_password) self.log_message(f"- {page_num}페이지: '{name}'님 (비밀번호: {birth_date_password}) ✓") success_count += 1 except Exception as e: new_doc.save(output_filename) self.log_message(f"- {page_num}페이지: '{name}'님 (비밀번호 설정 실패, 일반 파일로 저장) ⚠️") success_count += 1 else: new_doc.save(output_filename) self.log_message(f"- {page_num}페이지: '{name}'님 (생년월일 없음, 일반 파일로 저장) ⚠️") success_count += 1 else: new_doc.save(output_filename) self.log_message(f"- {page_num}페이지: '{name}'님 (일반 파일로 저장) ✓") success_count += 1 # 분할된 파일 정보 저장 self.split_files.append({ "name": name, "pdf_path": output_filename }) new_doc.close() else: self.log_message(f"- {page_num}페이지: 사원명을 찾지 못했습니다. ❌") fail_count += 1 doc.close() self.log_message("=" * 50) self.log_message(f"처리 완료! 성공: {success_count}개, 실패: {fail_count}개") if success_count > 0: # 이메일 탭의 파일 목록 업데이트 self.root.after(0, self.refresh_file_matching) # 자동 이메일 발송 확인 if self.auto_email_var.get(): self.root.after(0, self.auto_send_emails) messagebox.showinfo("완료", f"급여명세서 분할이 완료되었습니다.\n성공: {success_count}개, 실패: {fail_count}개") else: messagebox.showwarning("경고", "처리된 파일이 없습니다. PDF 형식을 확인해주세요.") except Exception as e: error_msg = f"오류가 발생했습니다: {str(e)}" self.log_message(error_msg) messagebox.showerror("오류", error_msg) finally: self.root.after(0, self.finish_processing) def finish_processing(self): """처리 완료 후 UI 상태 복원""" self.progress_bar.stop() self.progress_var.set("완료") self.process_button.config(state='normal') def refresh_file_matching(self): """파일 매칭 새로고침""" # 기존 아이템들 제거 for item in self.file_items: if hasattr(item, 'main_frame'): item.main_frame.destroy() self.file_items.clear() # 직원 목록 가져오기 employees = self.employee_manager.get_all_employees() # 분할된 파일들에 대해 매칭 for file_info in self.split_files: name = file_info["name"] pdf_path = file_info["pdf_path"] email = employees.get(name, "") # 파일 아이템 생성 file_item = PayslipFileItem( self.files_scrollable_frame, name, email, pdf_path, self.preview_manager, self.update_send_status ) self.file_items.append(file_item) self.update_send_status() def update_send_status(self): """발송 상태 업데이트""" total_count = len(self.file_items) send_count = sum(1 for item in self.file_items if item.send_var.get() and item.email) matched_count = sum(1 for item in self.file_items if item.email) status = f"전체: {total_count}명, 매칭됨: {matched_count}명, 발송대상: {send_count}명" self.email_status_var.set(status) def select_all_files(self): """전체 파일 선택""" for item in self.file_items: if item.email: # 이메일이 있는 경우만 item.send_var.set(True) item.on_send_status_change() def deselect_all_files(self): """전체 파일 해제""" for item in self.file_items: item.send_var.set(False) item.on_send_status_change() def test_email_connection(self): """이메일 연결 테스트""" username = self.email_user_var.get().strip() password = self.email_pass_var.get().strip() # 연결 테스트 시작 알림 self.email_status_var.set("🔌 이메일 서버 연결 테스트 중...") self.root.update() self.email_sender.set_credentials(username, password) success, message = self.email_sender.test_connection() if success: self.email_status_var.set("✅ 연결 성공! 이메일 발송 준비 완료") messagebox.showinfo("🎉 연결 성공", f"✅ SMTP 서버에 성공적으로 연결되었습니다!\n\n📧 이제 급여명세서를 이메일로 발송할 수 있습니다.\n\n설정: {username}") else: self.email_status_var.set("❌ 연결 실패 - 설정을 확인해주세요") messagebox.showerror("⚠️ 연결 실패", f"❌ SMTP 서버 연결에 실패했습니다.\n\n🔍 오류 내용:\n{message}\n\n📞 문제가 지속되면 IT팀에 문의해주세요.") def start_email_sending(self): """이메일 발송 시작""" # 발송할 데이터 수집 send_data = [] for item in self.file_items: data = item.get_send_data() if data: send_data.append(data) print(f"[DEBUG] 총 {len(send_data)}개의 이메일 발송 대상") if not send_data: messagebox.showwarning("경고", "발송할 파일이 없습니다.\n매칭된 직원을 선택해주세요.") return # 인증 정보 확인 username = self.email_user_var.get().strip() password = self.email_pass_var.get().strip() if not username or not password: messagebox.showwarning("경고", "이메일 사용자명과 비밀번호를 입력해주세요.") return # 발송 확인 result = messagebox.askyesno("확인", f"{len(send_data)}명에게 이메일을 발송하시겠습니까?") if not result: return # UI 비활성화 self.send_button.config(state='disabled') # 별도 스레드에서 발송 self.email_sender.set_credentials(username, password) thread = threading.Thread(target=self.send_emails, args=(send_data,)) thread.daemon = True thread.start() def send_emails(self, send_data): """이메일 발송 (별도 스레드)""" def progress_callback(current, total, name): self.root.after(0, lambda: self.email_status_var.set(f"발송 중... {current}/{total} ({name})")) try: results = self.email_sender.send_bulk_emails(send_data, progress_callback) success_count = len(results["success"]) failed_count = len(results["failed"]) # 발송 성공한 항목들의 UI 업데이트 for success_item in results["success"]: success_name = success_item.get("name") # 해당 직원의 파일 아이템 찾기 for file_item in self.file_items: if file_item.employee_name == success_name: self.root.after(0, lambda item=file_item: item.mark_as_sent()) break # 결과 메시지 message = f"이메일 발송 완료!\n\n성공: {success_count}명\n실패: {failed_count}명" if results["failed"]: message += "\n\n실패 목록:" for failed in results["failed"][:3]: # 최대 3개만 표시 message += f"\n- {failed['name']}: {failed['error'][:50]}..." if len(results["failed"]) > 3: message += f"\n... 및 {len(results['failed']) - 3}건 더" self.root.after(0, lambda: messagebox.showinfo("발송 완료", message)) self.root.after(0, lambda: self.email_status_var.set(f"발송 완료: 성공 {success_count}명, 실패 {failed_count}명")) except Exception as e: error_msg = f"이메일 발송 중 오류: {str(e)}" self.root.after(0, lambda: messagebox.showerror("발송 오류", error_msg)) self.root.after(0, lambda: self.email_status_var.set("발송 실패")) finally: self.root.after(0, lambda: self.send_button.config(state='normal')) def auto_send_emails(self): """자동 이메일 발송""" # 인증 정보가 있는지 확인 username = self.email_user_var.get().strip() password = self.email_pass_var.get().strip() if not username or not password: messagebox.showwarning("자동 발송 실패", "이메일 인증 정보가 설정되지 않았습니다.") return # 이메일 탭으로 전환 self.notebook.select(1) # 잠시 후 발송 시작 self.root.after(1000, self.start_email_sending) if __name__ == "__main__": root = TkinterDnD.Tk() app = PayslipSplitterGUI(root) root.mainloop()

          
        
    
    
Copyright Yona authors & © NAVER Corp. & NAVER LABS Supported by NAVER CLOUD PLATFORM

or
Sign in with github login with Google Sign in with Google
Reset password | Sign up