hehihoho3@gmail.com 08-28
first commit
@baf27ee7ab06e2a16c7c547272a7c7b48d4bdad2
 
README.md (added)
+++ README.md
@@ -0,0 +1,123 @@
+# 급여명세서 분할기 수정 가이드
+
+## �� 파일 구조
+```
+월급명세서_자동배포/
+├── 급여명세서분할기_v3_최종수정.exe    # 실행 파일
+├── email_config.json                   # 이메일 설정
+├── employees.json                       # 직원 목록
+├── payslip_splitter_gui_v3.py          # 메인 소스코드
+├── src/                                 # 핵심 모듈들
+│   ├── email_sender.py                  # 이메일 발송 로직
+│   ├── employee_manager.py              # 직원 관리
+│   └── pdf_preview.py                   # PDF 처리
+├── samples/                             # 샘플 PDF 파일들
+├── backup/                              # 백업 파일들
+├── build.bat                            # 빌드 스크립트
+├── requirements.txt                     # Python 패키지 목록
+└── 수정가이드.md                       # 이 파일
+```
+
+## ⚙️ 주요 설정 수정 방법
+
+### 1. 이메일 제목/내용 수정
+**파일**: `email_config.json`
+```json
+{
+  "email_template": {
+    "subject": "{month} 월급명세서 전달드립니다.",
+    "body": "여기에 이메일 내용 작성..."
+  }
+}
+```
+- `{month}`: 자동으로 다음달로 치환 (예: 8월 → 09월)
+- 제목과 내용을 자유롭게 수정 가능
+
+### 2. SMTP 서버 설정
+**파일**: `email_config.json`
+```json
+{
+  "smtp_server": "smtp.worksmobile.com",
+  "smtp_port": 465,
+  "use_ssl": true,
+  "sender_email": "noreply@munjaon.co.kr",
+  "sender_name": "급여관리팀"
+}
+```
+
+### 3. 직원 목록 초기 설정
+**파일**: `employees.json`
+```json
+[
+  {
+    "name": "홍길동",
+    "email": "hong@company.com",
+    "birth_date": "19900115"
+  }
+]
+```
+- 새 직원 추가 또는 기존 직원 정보 수정
+- `birth_date`: PDF 비밀번호로 사용됨 (YYYYMMDD 형식)
+
+## ��️ 고급 수정 (개발자용)
+
+### GUI 텍스트 수정
+**파일**: `payslip_splitter_gui_v3.py`
+- 버튼 텍스트, 라벨, 메시지 등 UI 요소 수정
+
+### 이메일 발송 로직 수정
+**파일**: `src/email_sender.py`
+- 날짜 추출 로직
+- 첨부파일 처리
+- SMTP 연결 설정
+
+### PDF 처리 로직 수정
+**파일**: `src/employee_manager.py`, `src/pdf_preview.py`
+- PDF 분할 로직
+- 직원명 추출 패턴
+- 파일명 생성 규칙
+
+## �� 개발 환경 설정 (소스코드 수정시)
+
+### Python 패키지 설치
+```bash
+pip install -r requirements.txt
+```
+
+**requirements.txt에 포함된 패키지들:**
+- `pymupdf` - PDF 처리 (PyMuPDF/fitz)
+- `pytesseract` - OCR 텍스트 인식
+- `Pillow` - 이미지 처리 
+- `pyinstaller` - exe 파일 생성
+- `tkinterdnd2` - GUI 드래그앤드롭
+- `pandas` - 데이터 처리
+- `openpyxl` - Excel 파일 처리
+- `pyperclip` - 클립보드 기능
+
+**참고**: exe 파일만 사용한다면 이 패키지들 설치 불필요
+
+## �� 빌드 방법
+
+### 자동 빌드 (권장)
+```batch
+build.bat
+```
+
+### 수동 빌드
+```batch
+pyinstaller --onefile --windowed --name "급여명세서분할기_v3_최종수정" --clean --exclude-module torch --exclude-module tensorflow --exclude-module sklearn --exclude-module scipy --exclude-module pandas payslip_splitter_gui_v3.py
+```
+
+## ⚠️ 주의사항
+
+1. **이메일 설정**: `email_config.json` 수정 후 프로그램 재시작 필요
+2. **직원 목록**: GUI에서도 수정 가능하지만, 대량 추가시 JSON 파일 직접 수정 권장
+3. **백업**: 중요한 수정 전 `backup/` 폴더 활용
+4. **테스트**: 수정 후 반드시 테스트 발송으로 확인
+
+## �� 문제 해결
+
+- **PDF 첨부 안됨**: 파일 경로에 한글 있는지 확인
+- **이메일 발송 실패**: SMTP 설정 및 인증 정보 확인  
+- **현재달 표시됨**: PDF 암호화로 인한 정상 동작 (다음달로 자동 변환됨)
+- **발송 완료 상태 이상**: 프로그램 재시작 후 다시 시도(No newline at end of file)
 
__pycache__/email_sender.cpython-313.pyc (Binary) (added)
+++ __pycache__/email_sender.cpython-313.pyc
Binary file is not shown
 
build.bat (added)
+++ build.bat
@@ -0,0 +1,66 @@
+@echo off
+chcp 65001 > nul
+cls
+echo ================================================
+echo  급여명세서 분할기 빌드 스크립트
+echo ================================================
+echo.
+
+echo [1/4] 이전 빌드 파일 정리 중...
+if exist build rmdir /s /q build
+if exist dist rmdir /s /q dist
+if exist *.spec del /q *.spec
+echo ✓ 정리 완료
+
+echo.
+echo [2/4] PyInstaller 실행 중...
+echo 잠시만 기다려주세요... (약 1-2분 소요)
+echo.
+
+pyinstaller --onefile --windowed --name "급여명세서분할기_v3_최종수정" --clean --exclude-module torch --exclude-module tensorflow --exclude-module sklearn --exclude-module scipy --exclude-module pandas payslip_splitter_gui_v3.py
+
+echo.
+if exist "dist\급여명세서분할기_v3_최종수정.exe" (
+    echo [3/4] exe 파일을 메인 폴더로 복사 중...
+    copy "dist\급여명세서분할기_v3_최종수정.exe" "급여명세서분할기_v3_최종수정.exe" > nul
+    echo ✓ 복사 완료
+    
+    echo.
+    echo [4/4] 임시 파일 정리 중...
+    rmdir /s /q build > nul 2>&1
+    rmdir /s /q dist > nul 2>&1
+    del /q *.spec > nul 2>&1
+    echo ✓ 정리 완료
+    
+    echo.
+    echo ================================================
+    echo  빌드 성공! ��
+    echo ================================================
+    echo 생성된 파일: 급여명세서분할기_v3_최종수정.exe
+    echo.
+    
+    for %%A in (급여명세서분할기_v3_최종수정.exe) do (
+        set size=%%~zA
+    )
+    
+    if defined size (
+        echo 파일 크기: %size% bytes
+    )
+    
+    echo.
+    echo 이제 exe 파일을 다른 컴퓨터에서도 사용할 수 있습니다.
+    
+) else (
+    echo.
+    echo ================================================
+    echo  빌드 실패! ❌
+    echo ================================================
+    echo PyInstaller 실행 중 오류가 발생했습니다.
+    echo 다음을 확인해주세요:
+    echo 1. Python이 설치되어 있는지
+    echo 2. 필요한 패키지가 설치되어 있는지 (pip install -r requirements.txt)
+    echo 3. payslip_splitter_gui_v3.py 파일이 있는지
+)
+
+echo.
+pause(No newline at end of file)
 
email_config.json (added)
+++ email_config.json
@@ -0,0 +1,11 @@
+{
+  "smtp_server": "smtp.worksmobile.com",
+  "smtp_port": 465,
+  "use_ssl": true,
+  "sender_email": "noreply@munjaon.co.kr",
+  "sender_name": "급여관리팀",
+  "email_template": {
+    "subject": "{month} 월급명세서 전달드립니다.",
+    "body": "안녕하세요.\n\n늘 맡은 자리에서 애써주심에 깊이 감사드립니다.\n이번 달 급여명세서를 PDF 파일로 첨부하여 보내드립니다.\n\n※ 첨부 파일의 비밀번호는 본인 생년월일 8자리입니다.\n   예) 1990년 1월 15일 → 19900115\n\n확인 중 궁금한 점이나 문의사항이 있으시면 언제든 편하게 연락 주시기 바랍니다.\n\n감사합니다.\n\n급여관리팀 드림"
+  }
+}(No newline at end of file)
 
employees.json (added)
+++ employees.json
@@ -0,0 +1,19 @@
+{
+  "김혜리": "khr2205@iten.co.kr",
+  "이준호": "tolag3@iten.co.kr",
+  "유인식": "smartyu@iten.co.kr",
+  "원영현": "dudgusw@iten.co.kr",
+  "유찬희": "ych@iten.co.kr",
+  "조현희": "hc3874@iten.co.kr",
+  "강영묵": "ymkang@iten.co.kr",
+  "조용준": "antelope@iten.co.kr",
+  "우영두": "rosehips@iten.co.kr",
+  "김상훈": "aricowiz@iten.co.kr",
+  "장영익": "yeongik@iten.co.kr",
+  "정다은": "jungde@iten.co.kr",
+  "이지우": "dlwldn1024@iten.co.kr",
+  "박진순": "jsp@iten.co.kr",
+  "정수빈": "dhgksk99@iten.co.kr",
+  "강민경": "kmk0522@iten.co.kr",
+  "이호영": "hylee@iten.co.kr"
+}(No newline at end of file)
 
payslip_splitter_gui_v3.py (added)
+++ payslip_splitter_gui_v3.py
@@ -0,0 +1,961 @@
+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()(No newline at end of file)
 
requirements.txt (added)
+++ requirements.txt
@@ -0,0 +1,8 @@
+pymupdf
+pytesseract
+Pillow
+pyinstaller
+tkinterdnd2
+pandas
+openpyxl
+pyperclip(No newline at end of file)
 
src/__pycache__/email_sender.cpython-313.pyc (Binary) (added)
+++ src/__pycache__/email_sender.cpython-313.pyc
Binary file is not shown
 
src/email_sender.py (added)
+++ src/email_sender.py
@@ -0,0 +1,354 @@
+import smtplib
+import os
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+from email.mime.base import MIMEBase
+from email import encoders
+from email.utils import formataddr
+from typing import Dict, List, Tuple
+import json
+import fitz  # PyMuPDF
+import re
+import urllib.parse
+
+def extract_payslip_date_from_pdf(pdf_path: str) -> str:
+    """PDF에서 급여명세서 날짜 정보 추출하여 다음달 표시 (예: "08월" → "09월")"""
+    
+    # 기본값 설정: 현재 날짜 기준 다음달
+    from datetime import datetime
+    current_month = datetime.now().month
+    if current_month == 12:
+        next_month = 1
+    else:
+        next_month = current_month + 1
+    default_month = f"{next_month:02d}월"
+    
+    try:
+        doc = fitz.open(pdf_path)
+        if doc.page_count == 0:
+            doc.close()
+            return default_month
+            
+        # 첫 페이지에서 텍스트 추출
+        page = doc[0]
+        text = page.get_text()
+        doc.close()
+        
+        # "YYYY년MM월분 급여명세서" 패턴 찾기
+        patterns = [
+            r'(\d{4})년(\d{1,2})월분\s*급여명세서',
+            r'(\d{4})년(\d{1,2})월\s*급여명세서',
+            r'급여명세서.*?(\d{4})년(\d{1,2})월',
+        ]
+        
+        for pattern in patterns:
+            match = re.search(pattern, text)
+            if match:
+                year, month = match.groups()
+                year_int = int(year)
+                month_int = int(month)
+                
+                # 다음달 계산 (12월이면 1월로, 연도도 +1)
+                if month_int == 12:
+                    next_month = 1
+                    next_year = year_int + 1
+                else:
+                    next_month = month_int + 1
+                    next_year = year_int
+                
+                month_str = f"{next_month:02d}월"
+                return month_str
+        
+        # PDF에서 텍스트 추출 실패시 현재 날짜 기준 다음달 반환
+        return default_month
+        
+    except Exception as e:
+        print(f"날짜 추출 실패 ({pdf_path}): {e}")
+        # 오류 발생시에도 현재 날짜 기준 다음달 반환
+        return default_month
+
+class EmailConfig:
+    """이메일 설정 관리 클래스"""
+    
+    def __init__(self, config_file="email_config.json"):
+        self.config_file = config_file
+        self.config = self.load_config()
+    
+    def load_config(self) -> Dict:
+        """설정 파일 로드"""
+        default_config = {
+            "smtp_server": "smtp.worksmobile.com",
+            "smtp_port": 465,
+            "use_ssl": True,
+            "sender_email": "noreply@munjaon.co.kr",
+            "sender_name": "급여관리팀",
+            "email_template": {
+                "subject": "{month} 월급명세서 전달드립니다.",
+                "body": """안녕하세요.
+
+늘 맡은 자리에서 애써주심에 깊이 감사드립니다.
+다음 달 급여명세서를 PDF 파일로 첨부하여 보내드립니다.
+
+※ 첨부 파일의 비밀번호는 본인 생년월일 8자리입니다.
+   예) 1990년 1월 15일 → 19900115
+
+확인 중 궁금한 점이나 문의사항이 있으시면 언제든 편하게 연락 주시기 바랍니다.
+
+감사합니다.
+
+급여관리팀 드림"""
+            }
+        }
+        
+        if os.path.exists(self.config_file):
+            try:
+                with open(self.config_file, 'r', encoding='utf-8') as f:
+                    loaded_config = json.load(f)
+                    # 기본 설정과 병합
+                    default_config.update(loaded_config)
+            except Exception as e:
+                print(f"설정 파일 로드 실패: {e}")
+        
+        self.save_config(default_config)
+        return default_config
+    
+    def save_config(self, config: Dict = None):
+        """설정 파일 저장"""
+        if config is None:
+            config = self.config
+        
+        try:
+            with open(self.config_file, 'w', encoding='utf-8') as f:
+                json.dump(config, f, ensure_ascii=False, indent=2)
+        except Exception as e:
+            print(f"설정 파일 저장 실패: {e}")
+    
+    def get_smtp_config(self) -> Dict:
+        """SMTP 설정 반환"""
+        return {
+            "server": self.config.get("smtp_server", "smtp.worksmobile.com"),
+            "port": self.config.get("smtp_port", 465),
+            "use_ssl": self.config.get("use_ssl", True)
+        }
+    
+    def get_sender_info(self) -> Dict:
+        """발송자 정보 반환"""
+        return {
+            "email": self.config.get("sender_email", "noreply@munjaon.co.kr"),
+            "name": self.config.get("sender_name", "급여관리팀")
+        }
+    
+    def get_email_template(self) -> Dict:
+        """이메일 템플릿 반환"""
+        return self.config.get("email_template", {})
+
+class PayslipEmailSender:
+    """급여명세서 이메일 발송 클래스"""
+    
+    def __init__(self, username: str = None, password: str = None):
+        self.config = EmailConfig()
+        self.username = username
+        self.password = password
+        self.smtp_config = self.config.get_smtp_config()
+        self.sender_info = self.config.get_sender_info()
+        self.template = self.config.get_email_template()
+    
+    def set_credentials(self, username: str, password: str):
+        """인증 정보 설정"""
+        self.username = username
+        self.password = password
+    
+    def create_email_message(self, recipient_email: str, recipient_name: str, 
+                           pdf_path: str, custom_subject: str = None, 
+                           custom_body: str = None) -> MIMEMultipart:
+        """이메일 메시지 생성"""
+        msg = MIMEMultipart()
+        
+        # 발송자 정보
+        sender_email = self.sender_info["email"]
+        sender_name = self.sender_info["name"]
+        msg['From'] = formataddr((sender_name, sender_email))
+        msg['To'] = recipient_email
+        
+        # 제목 - PDF에서 날짜 추출하여 동적 생성
+        if custom_subject:
+            subject = custom_subject
+        else:
+            month = extract_payslip_date_from_pdf(pdf_path) if pdf_path else "현재달"
+            subject_template = self.template.get("subject", "{month} 월급명세서 전달드립니다.")
+            subject = subject_template.format(month=month)
+        
+        msg['Subject'] = subject
+        
+        # 본문
+        body_template = custom_body or self.template.get("body", "안녕하세요, {name}님.\n\n월급명세서를 첨부하여 전달드립니다.")
+        body = body_template.format(name=recipient_name)
+        
+        msg.attach(MIMEText(body, 'plain', 'utf-8'))
+        
+        # 첨부파일
+        if pdf_path and os.path.exists(pdf_path):
+            try:
+                with open(pdf_path, "rb") as attachment:
+                    # PDF 전용 MIME 타입 사용
+                    part = MIMEBase('application', 'pdf')
+                    part.set_payload(attachment.read())
+                
+                encoders.encode_base64(part)
+                
+                filename = os.path.basename(pdf_path)
+                # 한국어 파일명 인코딩 처리
+                try:
+                    filename_encoded = filename.encode('ascii')
+                    filename_header = f'attachment; filename="{filename}"'
+                except UnicodeEncodeError:
+                    # 한국어 파일명의 경우 UTF-8로 인코딩
+                    import urllib.parse
+                    filename_encoded = urllib.parse.quote(filename)
+                    filename_header = f"attachment; filename*=UTF-8''{filename_encoded}"
+                
+                part.add_header('Content-Disposition', filename_header)
+                part.add_header('Content-Transfer-Encoding', 'base64')
+                
+                msg.attach(part)
+                print(f"PDF 첨부 성공: {filename} ({os.path.getsize(pdf_path)} bytes)")  # 디버그 로그
+            except Exception as e:
+                print(f"PDF 첨부 실패 ({pdf_path}): {e}")  # 오류 로그
+        else:
+            print(f"PDF 파일 없음 또는 경로 문제: {pdf_path}")
+        
+        return msg
+    
+    def send_single_email(self, recipient_email: str, recipient_name: str, 
+                         pdf_path: str, custom_subject: str = None, 
+                         custom_body: str = None) -> Tuple[bool, str]:
+        """단일 이메일 발송"""
+        if not self.username or not self.password:
+            return False, "인증 정보가 설정되지 않았습니다."
+        
+        if not os.path.exists(pdf_path):
+            return False, f"첨부파일을 찾을 수 없습니다: {pdf_path}"
+        
+        try:
+            # 이메일 메시지 생성
+            msg = self.create_email_message(recipient_email, recipient_name, 
+                                          pdf_path, custom_subject, custom_body)
+            
+            # SMTP 서버 연결 및 발송
+            if self.smtp_config["use_ssl"]:
+                server = smtplib.SMTP_SSL(self.smtp_config["server"], self.smtp_config["port"])
+            else:
+                server = smtplib.SMTP(self.smtp_config["server"], self.smtp_config["port"])
+                server.starttls()
+            
+            server.login(self.username, self.password)
+            
+            text = msg.as_string()
+            server.sendmail(self.sender_info["email"], recipient_email, text)
+            server.quit()
+            
+            return True, f"'{recipient_name}'님에게 이메일이 성공적으로 발송되었습니다."
+            
+        except Exception as e:
+            return False, f"이메일 발송 실패: {str(e)}"
+    
+    def send_bulk_emails(self, email_data: List[Dict], progress_callback=None) -> Dict:
+        """대량 이메일 발송
+        
+        Args:
+            email_data: [{"name": "이름", "email": "이메일", "pdf_path": "파일경로"}]
+            progress_callback: 진행상황 콜백 함수
+            
+        Returns:
+            {"success": [], "failed": []}
+        """
+        if not self.username or not self.password:
+            return {
+                "success": [],
+                "failed": [{"name": "모든 직원", "error": "인증 정보가 설정되지 않았습니다."}]
+            }
+        
+        results = {"success": [], "failed": []}
+        total_count = len(email_data)
+        
+        try:
+            # SMTP 서버 연결 (연결 유지)
+            if self.smtp_config["use_ssl"]:
+                server = smtplib.SMTP_SSL(self.smtp_config["server"], self.smtp_config["port"])
+            else:
+                server = smtplib.SMTP(self.smtp_config["server"], self.smtp_config["port"])
+                server.starttls()
+            
+            server.login(self.username, self.password)
+            
+            for i, data in enumerate(email_data):
+                name = data.get("name", "")
+                email = data.get("email", "")
+                pdf_path = data.get("pdf_path", "")
+                
+                try:
+                    if not os.path.exists(pdf_path):
+                        results["failed"].append({
+                            "name": name,
+                            "email": email,
+                            "error": f"첨부파일을 찾을 수 없음: {pdf_path}"
+                        })
+                        continue
+                    
+                    # 이메일 메시지 생성
+                    msg = self.create_email_message(email, name, pdf_path)
+                    
+                    # 발송
+                    text = msg.as_string()
+                    server.sendmail(self.sender_info["email"], email, text)
+                    
+                    results["success"].append({
+                        "name": name,
+                        "email": email,
+                        "pdf_path": pdf_path
+                    })
+                    
+                except Exception as e:
+                    results["failed"].append({
+                        "name": name,
+                        "email": email,
+                        "error": str(e)
+                    })
+                
+                # 진행상황 업데이트
+                if progress_callback:
+                    progress_callback(i + 1, total_count, name)
+            
+            server.quit()
+            
+        except Exception as e:
+            # 서버 연결 실패
+            for data in email_data:
+                if data.get("name") not in [item.get("name") for item in results["success"]]:
+                    results["failed"].append({
+                        "name": data.get("name", ""),
+                        "email": data.get("email", ""),
+                        "error": f"SMTP 서버 연결 실패: {str(e)}"
+                    })
+        
+        return results
+    
+    def test_connection(self) -> Tuple[bool, str]:
+        """SMTP 서버 연결 테스트"""
+        if not self.username or not self.password:
+            return False, "인증 정보가 설정되지 않았습니다."
+        
+        try:
+            if self.smtp_config["use_ssl"]:
+                server = smtplib.SMTP_SSL(self.smtp_config["server"], self.smtp_config["port"])
+            else:
+                server = smtplib.SMTP(self.smtp_config["server"], self.smtp_config["port"])
+                server.starttls()
+            
+            server.login(self.username, self.password)
+            server.quit()
+            
+            return True, "SMTP 서버 연결 성공!"
+            
+        except Exception as e:
+            return False, f"SMTP 서버 연결 실패: {str(e)}"(No newline at end of file)
 
src/employee_manager.py (added)
+++ src/employee_manager.py
@@ -0,0 +1,199 @@
+import json
+import os
+import csv
+from typing import Dict, List
+from datetime import datetime
+
+class EmployeeManager:
+    """직원 정보 관리 클래스"""
+    
+    def __init__(self, data_file="employees.json"):
+        self.data_file = data_file
+        self.employees = self.load_employees()
+    
+    def load_employees(self) -> Dict[str, str]:
+        """직원 데이터 로드 (JSON 파일에서)"""
+        if os.path.exists(self.data_file):
+            try:
+                with open(self.data_file, 'r', encoding='utf-8') as f:
+                    return json.load(f)
+            except Exception as e:
+                print(f"직원 데이터 로드 실패: {e}")
+                return {}
+        
+        # 기본 직원 데이터
+        default_employees = {
+            "김혜리": "khr2205@iten.co.kr",
+            "이준호": "tolag3@iten.co.kr",
+            "이호영": "hylee@iten.co.kr",
+            "유인식": "smartyu@iten.co.kr",
+            "원영현": "dudgusw@iten.co.kr",
+            "유찬희": "ych@iten.co.kr",
+            "조현희": "hc3874@iten.co.kr",
+            "강영묵": "ymkang@iten.co.kr",
+            "조용준": "antelope@iten.co.kr",
+            "우영두": "rosehips@iten.co.kr",
+            "김상훈": "aricowiz@iten.co.kr",
+            "장영익": "yeongik@iten.co.kr",
+            "정다은": "jungde@iten.co.kr",
+            "이지우": "dlwldn1024@iten.co.kr",
+            "박진순": "jsp@iten.co.kr",
+            "정수빈": "dhgksk99@iten.co.kr",
+            "강민경": "kmk0522@iten.co.kr"
+        }
+        
+        # 기본 데이터 저장
+        self.save_employees_data(default_employees)
+        return default_employees
+    
+    def save_employees(self):
+        """직원 데이터 저장"""
+        self.save_employees_data(self.employees)
+    
+    def save_employees_data(self, data: Dict[str, str]):
+        """직원 데이터를 파일에 저장"""
+        try:
+            with open(self.data_file, 'w', encoding='utf-8') as f:
+                json.dump(data, f, ensure_ascii=False, indent=2)
+            
+            # 백업 CSV 파일도 생성
+            self.backup_to_csv(data)
+        except Exception as e:
+            print(f"직원 데이터 저장 실패: {e}")
+    
+    def backup_to_csv(self, data: Dict[str, str]):
+        """CSV 백업 파일 생성"""
+        try:
+            backup_file = "employees_backup.csv"
+            with open(backup_file, 'w', newline='', encoding='utf-8') as f:
+                writer = csv.writer(f)
+                writer.writerow(['이름', '이메일', '백업일시'])
+                current_time = datetime.now().strftime('%Y-%m-%d %H:%M')
+                for name, email in data.items():
+                    writer.writerow([name, email, current_time])
+        except Exception as e:
+            print(f"CSV 백업 실패: {e}")
+    
+    def add_employee(self, name: str, email: str) -> bool:
+        """직원 추가"""
+        if name.strip() in self.employees:
+            return False  # 이미 존재
+        
+        self.employees[name.strip()] = email.strip()
+        self.save_employees()
+        return True
+    
+    def update_employee(self, old_name: str, new_name: str, email: str) -> bool:
+        """직원 정보 수정"""
+        if old_name not in self.employees:
+            return False
+        
+        old_name = old_name.strip()
+        new_name = new_name.strip()
+        email = email.strip()
+        
+        # 이름이 변경되는 경우
+        if old_name != new_name:
+            if new_name in self.employees:
+                return False  # 새 이름이 이미 존재
+            del self.employees[old_name]
+        
+        self.employees[new_name] = email
+        self.save_employees()
+        return True
+    
+    def delete_employee(self, name: str) -> bool:
+        """직원 삭제"""
+        if name in self.employees:
+            del self.employees[name]
+            self.save_employees()
+            return True
+        return False
+    
+    def get_employee_email(self, name: str) -> str:
+        """직원 이메일 조회"""
+        return self.employees.get(name.strip(), "")
+    
+    def get_all_employees(self) -> Dict[str, str]:
+        """전체 직원 목록 반환"""
+        return self.employees.copy()
+    
+    def search_employees(self, query: str) -> Dict[str, str]:
+        """직원 검색"""
+        query = query.lower().strip()
+        if not query:
+            return self.get_all_employees()
+        
+        result = {}
+        for name, email in self.employees.items():
+            if query in name.lower() or query in email.lower():
+                result[name] = email
+        return result
+    
+    def import_from_csv(self, file_path: str) -> tuple:
+        """CSV 파일에서 직원 정보 가져오기"""
+        success_count = 0
+        error_list = []
+        
+        try:
+            encodings = ['utf-8', 'cp949', 'euc-kr']
+            for encoding in encodings:
+                try:
+                    with open(file_path, 'r', encoding=encoding) as f:
+                        reader = csv.reader(f)
+                        header = next(reader, None)  # 헤더 스킵
+                        
+                        for row_num, row in enumerate(reader, 2):
+                            if len(row) >= 2:
+                                name = row[0].strip()
+                                email = row[1].strip()
+                                
+                                if name and email and '@' in email:
+                                    if self.add_employee_silent(name, email):
+                                        success_count += 1
+                                    else:
+                                        error_list.append(f"행 {row_num}: '{name}' 이미 존재")
+                                else:
+                                    error_list.append(f"행 {row_num}: 유효하지 않은 데이터")
+                    break
+                except UnicodeDecodeError:
+                    continue
+                    
+        except Exception as e:
+            error_list.append(f"파일 읽기 오류: {str(e)}")
+        
+        if success_count > 0:
+            self.save_employees()
+        
+        return success_count, error_list
+    
+    def add_employee_silent(self, name: str, email: str) -> bool:
+        """직원 추가 (저장하지 않음, 배치 처리용)"""
+        if name.strip() in self.employees:
+            return False
+        
+        self.employees[name.strip()] = email.strip()
+        return True
+    
+    def export_to_csv(self, file_path: str) -> bool:
+        """CSV 파일로 내보내기"""
+        try:
+            with open(file_path, 'w', newline='', encoding='utf-8') as f:
+                writer = csv.writer(f)
+                writer.writerow(['이름', '이메일'])  # 헤더
+                
+                for name, email in sorted(self.employees.items()):
+                    writer.writerow([name, email])
+            return True
+        except Exception as e:
+            print(f"CSV 내보내기 실패: {e}")
+            return False
+    
+    def get_employee_count(self) -> int:
+        """직원 수 반환"""
+        return len(self.employees)
+    
+    def clear_all_employees(self):
+        """모든 직원 정보 삭제"""
+        self.employees.clear()
+        self.save_employees()(No newline at end of file)
 
src/pdf_preview.py (added)
+++ src/pdf_preview.py
@@ -0,0 +1,331 @@
+import fitz  # PyMuPDF
+from PIL import Image, ImageTk
+import io
+import os
+import tkinter as tk
+from tkinter import ttk, messagebox
+import platform
+import subprocess
+
+class PDFPreviewManager:
+    """PDF 미리보기 관리 클래스"""
+    
+    def __init__(self):
+        self.preview_cache = {}  # 미리보기 이미지 캐싱
+    
+    def get_pdf_info(self, pdf_path: str) -> dict:
+        """PDF 기본 정보 추출"""
+        try:
+            if not pdf_path or not os.path.exists(pdf_path):
+                return {
+                    'error': 'PDF 파일을 찾을 수 없습니다.', 
+                    'page_count': 0, 
+                    'file_size': 0,
+                    'is_encrypted': False,
+                    'title': '',
+                    'author': '',
+                    'creation_date': '',
+                    'file_name': os.path.basename(pdf_path) if pdf_path else '',
+                    'file_path': pdf_path or ''
+                }
+            
+            doc = fitz.open(pdf_path)
+            metadata = doc.metadata or {}
+            info = {
+                'page_count': doc.page_count,
+                'file_size': os.path.getsize(pdf_path),
+                'is_encrypted': doc.is_encrypted,
+                'title': metadata.get('title', '') or '',
+                'author': metadata.get('author', '') or '',
+                'creation_date': metadata.get('creationDate', '') or '',
+                'file_name': os.path.basename(pdf_path),
+                'file_path': pdf_path
+            }
+            doc.close()
+            return info
+        except Exception as e:
+            return {
+                'error': str(e), 
+                'page_count': 0, 
+                'file_size': 0,
+                'is_encrypted': False,
+                'title': '',
+                'author': '',
+                'creation_date': '',
+                'file_name': os.path.basename(pdf_path) if pdf_path else '',
+                'file_path': pdf_path or ''
+            }
+    
+    def create_preview_thumbnail(self, pdf_path: str, target_size=(250, 350)) -> Image.Image:
+        """PDF 첫 페이지 썸네일 생성"""
+        cache_key = f"{pdf_path}_{target_size}"
+        
+        # 캐시에 있으면 반환
+        if cache_key in self.preview_cache:
+            return self.preview_cache[cache_key]
+        
+        try:
+            doc = fitz.open(pdf_path)
+            if doc.page_count == 0:
+                doc.close()
+                return None
+                
+            page = doc[0]  # 첫 페이지
+            
+            # 미리보기 크기에 맞게 해상도 계산
+            zoom_x = target_size[0] / page.rect.width
+            zoom_y = target_size[1] / page.rect.height
+            zoom = min(zoom_x, zoom_y) * 1.5  # 약간 높은 해상도
+            
+            mat = fitz.Matrix(zoom, zoom)
+            pix = page.get_pixmap(matrix=mat)
+            img_data = pix.tobytes("png")
+            
+            # PIL Image로 변환
+            image = Image.open(io.BytesIO(img_data))
+            image.thumbnail(target_size, Image.Resampling.LANCZOS)
+            
+            doc.close()
+            
+            # 캐시에 저장
+            self.preview_cache[cache_key] = image
+            return image
+            
+        except Exception as e:
+            print(f"미리보기 생성 실패 ({pdf_path}): {e}")
+            return None
+    
+    def format_file_size(self, size_bytes: int) -> str:
+        """파일 크기를 읽기 쉬운 형태로 변환"""
+        if size_bytes < 1024:
+            return f"{size_bytes}B"
+        elif size_bytes < 1024**2:
+            return f"{size_bytes/1024:.1f}KB"
+        else:
+            return f"{size_bytes/(1024**2):.1f}MB"
+    
+    def clear_cache(self):
+        """캐시 클리어"""
+        self.preview_cache.clear()
+    
+    def open_file_with_system(self, pdf_path: str) -> bool:
+        """시스템 기본 프로그램으로 파일 열기"""
+        try:
+            if platform.system() == 'Windows':
+                os.startfile(pdf_path)
+            elif platform.system() == 'Darwin':  # macOS
+                subprocess.run(['open', pdf_path])
+            else:  # Linux
+                subprocess.run(['xdg-open', pdf_path])
+            return True
+        except Exception as e:
+            print(f"파일 열기 실패 ({pdf_path}): {e}")
+            return False
+
+class PDFPreviewDialog:
+    """PDF 미리보기 다이얼로그"""
+    
+    def __init__(self, parent, pdf_path: str, employee_name: str, preview_manager: PDFPreviewManager):
+        self.parent = parent
+        self.pdf_path = pdf_path
+        self.employee_name = employee_name
+        self.preview_manager = preview_manager
+        
+        # PDF 정보 가져오기
+        self.pdf_info = preview_manager.get_pdf_info(pdf_path)
+        
+        # 다이얼로그 창 생성
+        self.window = tk.Toplevel(parent)
+        self.window.title(f"미리보기 - {employee_name}")
+        self.window.geometry("450x650")
+        self.window.resizable(True, True)
+        
+        # 창을 모달로 설정
+        self.window.transient(parent)
+        self.window.grab_set()
+        
+        # 창을 화면 중앙에 위치
+        self.center_window()
+        
+        self.setup_ui()
+    
+    def center_window(self):
+        """창을 화면 중앙에 위치시키기"""
+        self.window.update_idletasks()
+        width = self.window.winfo_width()
+        height = self.window.winfo_height()
+        pos_x = (self.window.winfo_screenwidth() // 2) - (width // 2)
+        pos_y = (self.window.winfo_screenheight() // 2) - (height // 2)
+        self.window.geometry(f'{width}x{height}+{pos_x}+{pos_y}')
+    
+    def setup_ui(self):
+        """UI 구성"""
+        main_frame = ttk.Frame(self.window, padding=10)
+        main_frame.pack(fill='both', expand=True)
+        
+        # 상단: 파일 정보
+        info_frame = ttk.LabelFrame(main_frame, text="�� 파일 정보", padding=10)
+        info_frame.pack(fill='x', pady=(0, 10))
+        
+        # 정보 표시
+        info_grid = ttk.Frame(info_frame)
+        info_grid.pack(fill='x')
+        
+        # 파일명
+        ttk.Label(info_grid, text="파일명:", font=('맑은 고딕', 9, 'bold')).grid(
+            row=0, column=0, sticky='w', padx=(0, 10)
+        )
+        ttk.Label(info_grid, text=self.pdf_info.get('file_name', '알 수 없음')).grid(
+            row=0, column=1, sticky='w'
+        )
+        
+        # 직원명
+        ttk.Label(info_grid, text="직원명:", font=('맑은 고딕', 9, 'bold')).grid(
+            row=1, column=0, sticky='w', padx=(0, 10), pady=2
+        )
+        ttk.Label(info_grid, text=self.employee_name).grid(
+            row=1, column=1, sticky='w', pady=2
+        )
+        
+        # 페이지 수
+        page_count = self.pdf_info.get('page_count', 0)
+        ttk.Label(info_grid, text="페이지 수:", font=('맑은 고딕', 9, 'bold')).grid(
+            row=2, column=0, sticky='w', padx=(0, 10), pady=2
+        )
+        ttk.Label(info_grid, text=f"{page_count}페이지").grid(
+            row=2, column=1, sticky='w', pady=2
+        )
+        
+        # 파일 크기
+        file_size = self.preview_manager.format_file_size(self.pdf_info.get('file_size', 0))
+        ttk.Label(info_grid, text="파일 크기:", font=('맑은 고딕', 9, 'bold')).grid(
+            row=3, column=0, sticky='w', padx=(0, 10), pady=2
+        )
+        ttk.Label(info_grid, text=file_size).grid(
+            row=3, column=1, sticky='w', pady=2
+        )
+        
+        # 암호화 상태
+        if self.pdf_info.get('is_encrypted'):
+            ttk.Label(info_grid, text="보안:", font=('맑은 고딕', 9, 'bold')).grid(
+                row=4, column=0, sticky='w', padx=(0, 10), pady=2
+            )
+            security_label = ttk.Label(info_grid, text="�� 비밀번호 보호됨", foreground='orange')
+            security_label.grid(row=4, column=1, sticky='w', pady=2)
+        
+        # 중앙: 미리보기 이미지
+        preview_frame = ttk.LabelFrame(main_frame, text="�� 첫 페이지 미리보기", padding=10)
+        preview_frame.pack(fill='both', expand=True, pady=(0, 10))
+        
+        # 스크롤 가능한 캔버스 생성
+        canvas_frame = ttk.Frame(preview_frame)
+        canvas_frame.pack(fill='both', expand=True)
+        
+        self.canvas = tk.Canvas(canvas_frame, bg='white', relief='sunken', bd=2)
+        scrollbar_v = ttk.Scrollbar(canvas_frame, orient='vertical', command=self.canvas.yview)
+        scrollbar_h = ttk.Scrollbar(canvas_frame, orient='horizontal', command=self.canvas.xview)
+        
+        self.canvas.configure(yscrollcommand=scrollbar_v.set, xscrollcommand=scrollbar_h.set)
+        
+        # 미리보기 이미지 로드 및 표시
+        self.load_preview_image()
+        
+        # 그리드 배치
+        self.canvas.grid(row=0, column=0, sticky='nsew')
+        scrollbar_v.grid(row=0, column=1, sticky='ns')
+        scrollbar_h.grid(row=1, column=0, sticky='ew')
+        
+        canvas_frame.grid_rowconfigure(0, weight=1)
+        canvas_frame.grid_columnconfigure(0, weight=1)
+        
+        # 하단: 버튼들
+        button_frame = ttk.Frame(main_frame)
+        button_frame.pack(fill='x', pady=(10, 0))
+        
+        # 버튼 배치
+        ttk.Button(button_frame, text="�� 시스템으로 열기", 
+                  command=self.open_with_system).pack(side='left', padx=(0, 5))
+        ttk.Button(button_frame, text="�� 폴더 열기", 
+                  command=self.open_folder).pack(side='left', padx=5)
+        ttk.Button(button_frame, text="�� 새로고침", 
+                  command=self.refresh_preview).pack(side='left', padx=5)
+        
+        # 오른쪽에 닫기 버튼
+        ttk.Button(button_frame, text="닫기", 
+                  command=self.close_dialog).pack(side='right')
+    
+    def load_preview_image(self):
+        """미리보기 이미지 로드"""
+        try:
+            if 'error' in self.pdf_info:
+                # 오류 메시지 표시
+                self.canvas.create_text(200, 100, 
+                                      text=f"미리보기를 생성할 수 없습니다.\n{self.pdf_info['error']}", 
+                                      fill='red', font=('맑은 고딕', 12), anchor='center')
+                return
+            
+            # 미리보기 이미지 생성
+            thumbnail = self.preview_manager.create_preview_thumbnail(self.pdf_path, (350, 500))
+            
+            if thumbnail:
+                # PIL Image를 PhotoImage로 변환
+                self.photo = ImageTk.PhotoImage(thumbnail)
+                
+                # 캔버스에 이미지 표시
+                self.canvas.delete("all")  # 기존 내용 제거
+                self.canvas.create_image(thumbnail.width//2, thumbnail.height//2, 
+                                       image=self.photo, anchor='center')
+                
+                # 스크롤 영역 설정
+                self.canvas.configure(scrollregion=self.canvas.bbox("all"))
+                
+            else:
+                # 미리보기 실패 메시지
+                self.canvas.create_text(200, 100, 
+                                      text="미리보기를 생성할 수 없습니다.\nPDF 파일이 손상되었거나\n지원하지 않는 형식일 수 있습니다.", 
+                                      fill='red', font=('맑은 고딕', 12), anchor='center')
+                
+        except Exception as e:
+            # 예외 발생시 오류 메시지 표시
+            self.canvas.create_text(200, 100, 
+                                  text=f"미리보기 로드 실패:\n{str(e)}", 
+                                  fill='red', font=('맑은 고딕', 12), anchor='center')
+    
+    def open_with_system(self):
+        """시스템 기본 프로그램으로 열기"""
+        success = self.preview_manager.open_file_with_system(self.pdf_path)
+        if not success:
+            messagebox.showerror("오류", "파일을 열 수 없습니다.\n기본 PDF 뷰어가 설치되어 있는지 확인해주세요.")
+    
+    def open_folder(self):
+        """파일이 있는 폴더 열기"""
+        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)}")
+    
+    def refresh_preview(self):
+        """미리보기 새로고침"""
+        # 캐시에서 제거
+        cache_key = f"{self.pdf_path}_(350, 500)"
+        if cache_key in self.preview_manager.preview_cache:
+            del self.preview_manager.preview_cache[cache_key]
+        
+        # PDF 정보 다시 로드
+        self.pdf_info = self.preview_manager.get_pdf_info(self.pdf_path)
+        
+        # 미리보기 이미지 다시 로드
+        self.load_preview_image()
+        
+        messagebox.showinfo("새로고침", "미리보기가 새로고침되었습니다.")
+    
+    def close_dialog(self):
+        """다이얼로그 닫기"""
+        self.window.grab_release()  # 모달 해제
+        self.window.destroy()(No newline at end of file)
 
test/employees.json (added)
+++ test/employees.json
@@ -0,0 +1,19 @@
+{
+  "김혜리": "khr2205@iten.co.kr",
+  "이준호": "tolag3@iten.co.kr",
+  "유인식": "smartyu@iten.co.kr",
+  "원영현": "dudgusw@iten.co.kr",
+  "유찬희": "ych@iten.co.kr",
+  "조현희": "hc3874@iten.co.kr",
+  "강영묵": "ymkang@iten.co.kr",
+  "조용준": "antelope@iten.co.kr",
+  "우영두": "rosehips@iten.co.kr",
+  "김상훈": "aricowiz@iten.co.kr",
+  "장영익": "yeongik@iten.co.kr",
+  "정다은": "jungde@iten.co.kr",
+  "이지우": "dlwldn1024@iten.co.kr",
+  "박진순": "jsp@iten.co.kr",
+  "정수빈": "dhgksk99@iten.co.kr",
+  "강민경": "kmk0522@iten.co.kr",
+  "이호영": "hylee@iten.co.kr"
+}(No newline at end of file)
 
test/employees_backup.csv (added)
+++ test/employees_backup.csv
@@ -0,0 +1,18 @@
+이름,이메일,백업일시
+김혜리,khr2205@iten.co.kr,2025-08-28 16:41
+이준호,tolag3@iten.co.kr,2025-08-28 16:41
+유인식,smartyu@iten.co.kr,2025-08-28 16:41
+원영현,dudgusw@iten.co.kr,2025-08-28 16:41
+유찬희,ych@iten.co.kr,2025-08-28 16:41
+조현희,hc3874@iten.co.kr,2025-08-28 16:41
+강영묵,ymkang@iten.co.kr,2025-08-28 16:41
+조용준,antelope@iten.co.kr,2025-08-28 16:41
+우영두,rosehips@iten.co.kr,2025-08-28 16:41
+김상훈,aricowiz@iten.co.kr,2025-08-28 16:41
+장영익,yeongik@iten.co.kr,2025-08-28 16:41
+정다은,jungde@iten.co.kr,2025-08-28 16:41
+이지우,dlwldn1024@iten.co.kr,2025-08-28 16:41
+박진순,jsp@iten.co.kr,2025-08-28 16:41
+정수빈,dhgksk99@iten.co.kr,2025-08-28 16:41
+강민경,kmk0522@iten.co.kr,2025-08-28 16:41
+이호영,hylee@iten.co.kr,2025-08-28 16:41
 
test/급여명세서.pdf (Binary) (added)
+++ test/급여명세서.pdf
Binary file is not shown
Add a comment
List