File name
Commit message
Commit date
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()