Chapter 5: 2-패널 레이아웃과 미리보기¶
이번 챕터에서는 Chapter 4의 GUI를 바탕으로 전문적인 2-패널 레이아웃과 실시간 미리보기 기능을 구현합니다. 최종 KRenamer와 유사한 구조로 발전시켜 더 나은 사용자 경험을 제공해보겠습니다.
🎯 학습 목표¶
- 2-패널 레이아웃으로 전문적인 인터페이스 구현
- 실시간 미리보기로 안전성과 편의성 확보
- 파일 관리 엔진 분리로 코드 구조 개선
- 상태 관리와 데이터 흐름 최적화
- 최종 KRenamer 수준의 완성도 달성
🏗️ Chapter 4에서 Chapter 5로의 발전¶
구조적 변화¶
| 측면 | Chapter 4 | Chapter 5 |
|---|---|---|
| 레이아웃 | 단일 패널 세로 배치 | 2-패널 좌우 분할 |
| 미리보기 | 별도 창/텍스트 형태 | 실시간 테이블 형태 |
| 코드 구조 | GUI와 로직 혼재 | 엔진과 GUI 분리 |
| 데이터 흐름 | 수동 업데이트 | 자동 실시간 동기화 |
| 사용자 경험 | 기본적 | 전문적 |
🚀 Step-by-Step 개발 과정¶
이번 챕터에서 구현할 5단계¶
- Step 1: 파일 관리 엔진 설계와 구현
- Step 2: 2-패널 레이아웃 기본 구조
- Step 3: 실시간 미리보기 시스템
- Step 4: 고급 UI 개선과 인터랙션
- Step 5: 완성된 전문적 인터페이스
💡 이번 챕터에서 배울 핵심 개념¶
아키텍처 설계 원칙¶
- 관심사 분리: GUI와 비즈니스 로직을 완전히 분리
- 데이터 바인딩: GUI와 엔진 간 실시간 동기화
- 이벤트 중심 설계: 사용자 입력에 따른 자동 업데이트
- 상태 관리: 일관성 있는 데이터 흐름 보장
2-패널 레이아웃 설계 원칙¶
- 공간 효율성: 좌우 분할로 화면 공간 최대 활용
- 기능적 분리: 입력/제어와 결과/미리보기 영역 구분
- 시각적 차층: 기능별로 그룹화된 인터페이스
- 사용자 흐름: 좌쪽에서 오른쪽으로 자연스러운 작업 순서
실시간 미리보기 시스템¶
- 즉시 피드백: 옵션 변경 시 실시간 결과 표시
- 유효성 검사: 안전하지 않은 변경 사전 에러 표시
- 시각적 구분: 상태별 색상 코딩으로 직관적 이해
- 대량 처리: 수백 개 파일도 빠른 미리보기 지원
프로젝트 구조¶
src/chapter5/
├── step1_basic_layout.py # 기본 2-패널 레이아웃
├── step2_engine_integration.py # 엔진 통합
├── step3_real_time_preview.py # 실시간 미리보기
├── step4_advanced_ui.py # 고급 UI 기능
├── step5_professional.py # 완성된 인터페이스
└── rename_engine.py # 파일 관리 엔진
🔧 Step 1: 파일 관리 엔진 설계와 구현¶
목표¶
Chapter 5에서 사용할 파일 관리 엔진을 설계하고 구현합니다. 이 엔진은 GUI와 완전히 분리되어 재사용 가능하고 테스트하기 쉬운 구조로 만들어집니다.
rename_engine.py - 파일 관리 엔진¶
import os
from typing import List, Tuple, Dict, Any
from pathlib import Path
class RenameEngine:
"""파일명 변경을 처리하는 엔진 클래스"""
def __init__(self):
# 파일 목록 관리
self.files: List[str] = []
# 이름 변경 옵션들
self.prefix = ""
self.suffix = ""
self.find_text = ""
self.replace_text = ""
self.use_numbering = False
self.number_start = 1
self.number_digits = 3
# 콜백 함수들 (선택적)
self.on_files_changed = None
self.on_options_changed = None
# 파일 관리 메서드들
def add_file(self, file_path: str) -> bool:
"""파일을 목록에 추가"""
if os.path.isfile(file_path) and file_path not in self.files:
self.files.append(file_path)
self._notify_files_changed()
return True
return False
def add_files(self, file_paths: List[str]) -> int:
"""여러 파일을 대량 추가"""
added_count = 0
for file_path in file_paths:
if self.add_file(file_path):
added_count += 1
return added_count
def remove_file(self, file_path: str) -> bool:
"""파일을 목록에서 제거"""
if file_path in self.files:
self.files.remove(file_path)
self._notify_files_changed()
return True
return False
def remove_files_by_indices(self, indices: List[int]) -> int:
"""인덱스로 여러 파일 제거"""
# 역순으로 정렬해서 제거 (인덱스 꼬임 방지)
removed_count = 0
for index in sorted(indices, reverse=True):
if 0 <= index < len(self.files):
del self.files[index]
removed_count += 1
if removed_count > 0:
self._notify_files_changed()
return removed_count
def clear_files(self):
"""모든 파일 제거"""
self.files.clear()
self._notify_files_changed()
def get_file_count(self) -> int:
"""파일 개수 반환"""
return len(self.files)
# 옵션 설정 메서드들
def set_prefix(self, prefix: str):
"""접두사 설정"""
if self.prefix != prefix:
self.prefix = prefix
self._notify_options_changed()
def set_suffix(self, suffix: str):
"""접미사 설정"""
if self.suffix != suffix:
self.suffix = suffix
self._notify_options_changed()
def set_find_replace(self, find_text: str, replace_text: str):
"""찾기/바꾸기 설정"""
changed = self.find_text != find_text or self.replace_text != replace_text
self.find_text = find_text
self.replace_text = replace_text
if changed:
self._notify_options_changed()
def set_numbering(self, use_numbering: bool, start: int = 1, digits: int = 3):
"""순번 매기기 설정"""
changed = (self.use_numbering != use_numbering or
self.number_start != start or
self.number_digits != digits)
self.use_numbering = use_numbering
self.number_start = start
self.number_digits = digits
if changed:
self._notify_options_changed()
# 파일명 변경 로직
def generate_new_name(self, original_filename: str, file_index: int = 0) -> str:
"""원본 파일명을 새로운 이름으로 변경"""
name, ext = os.path.splitext(original_filename)
# 1단계: 찾기/바꾸기 적용
if self.find_text:
name = name.replace(self.find_text, self.replace_text)
# 2단계: 순번 매기기 적용
if self.use_numbering:
number = str(self.number_start + file_index).zfill(self.number_digits)
name = f"{name}_{number}"
# 3단계: 접두사/접미사 추가
new_name = f"{self.prefix}{name}{self.suffix}{ext}"
return new_name
def is_valid_filename(self, filename: str, original_path: str) -> Tuple[bool, str]:
"""파일명 유효성 검사"""
# 1. 빈 파일명 검사
if not filename.strip():
return False, "빈 파일명"
# 2. Windows 금지 문자 검사
forbidden_chars = '<>:"/\\|?*'
for char in forbidden_chars:
if char in filename:
return False, f"금지된 문자 '{char}' 포함"
# 3. 길이 검사
if len(filename) > 255:
return False, "파일명이 너무 김 (255자 초과)"
# 4. 중복 파일명 검사
directory = os.path.dirname(original_path)
new_path = os.path.join(directory, filename)
if os.path.exists(new_path) and new_path != original_path:
return False, "동일한 이름의 파일이 이미 존재"
return True, ""
def generate_preview(self) -> List[Tuple[str, str, bool, str]]:
"""모든 파일의 미리보기 생성"""
preview_list = []
for i, file_path in enumerate(self.files):
original_name = os.path.basename(file_path)
new_name = self.generate_new_name(original_name, i)
is_valid, error_msg = self.is_valid_filename(new_name, file_path)
preview_list.append((original_name, new_name, is_valid, error_msg))
return preview_list
def execute_rename(self) -> Dict[str, Any]:
"""실제 파일명 변경 실행"""
results = {
'success': 0,
'failed': 0,
'errors': [],
'renamed_files': [] # (원본경로, 새경로) 튜플 목록
}
preview = self.generate_preview()
for i, (original_name, new_name, is_valid, error_msg) in enumerate(preview):
original_path = self.files[i]
if not is_valid:
results['failed'] += 1
results['errors'].append(f"{original_name}: {error_msg}")
continue
# 변경이 필요한지 확인
if original_name == new_name:
continue # 변경 사항 없음
directory = os.path.dirname(original_path)
new_path = os.path.join(directory, new_name)
try:
os.rename(original_path, new_path)
# 성공 시 내부 목록 업데이트
self.files[i] = new_path
results['success'] += 1
results['renamed_files'].append((original_path, new_path))
except Exception as e:
results['failed'] += 1
results['errors'].append(f"{original_name}: {str(e)}")
if results['success'] > 0:
self._notify_files_changed()
return results
# 콜백 알림 메서드들
def _notify_files_changed(self):
"""파일 목록 변경 알림"""
if self.on_files_changed:
self.on_files_changed()
def _notify_options_changed(self):
"""옵션 변경 알림"""
if self.on_options_changed:
self.on_options_changed()
# 유틸리티 메서드들
def get_statistics(self) -> Dict[str, Any]:
"""파일 목록 통계 정보"""
if not self.files:
return {'total_files': 0, 'total_size': 0, 'file_types': {}}
total_size = 0
file_types = {}
for file_path in self.files:
try:
# 파일 크기
total_size += os.path.getsize(file_path)
# 파일 형식
ext = os.path.splitext(file_path)[1].lower()
if not ext:
ext = '(확장자 없음)'
file_types[ext] = file_types.get(ext, 0) + 1
except OSError:
continue # 파일에 접근할 수 없음
return {
'total_files': len(self.files),
'total_size': total_size,
'file_types': file_types
}
def reset_options(self):
"""모든 옵션 초기화"""
self.prefix = ""
self.suffix = ""
self.find_text = ""
self.replace_text = ""
self.use_numbering = False
self.number_start = 1
self.number_digits = 3
self._notify_options_changed()
실행 결과¶

Step 1에서는 GUI와 완전히 분리된 파일 관리 엔진을 구현했습니다. 이 엔진은 파일 목록 관리, 이름 변경 옵션 설정, 미리보기 생성, 실제 파일명 변경 등의 모든 비즈니스 로직을 담당합니다.
💡 Step 1에서 배운 엔진 설계 원칙¶
- 단일 책임 원칙: 각 메서드는 하나의 명확한 역할만 담당
- 데이터 언커플링: GUI와 완전히 독립적인 로직 계층
- 이벤트 기반: 콜백을 통한 비동기 알림 시스템
- 오류 처리: 명시적인 예외 처리와 상세한 오류 메시지
- 확장성: 새로운 기능 추가가 쉬운 모듈러 구조
🎨 Step 2: 2-패널 레이아웃 기본 구조¶
목표¶
Chapter 4의 단일 패널 구조를 좌우 분할된 2-패널 구조로 발전시킵니다. 왼쪽에는 파일 관리와 옵션 설정, 오른쪽에는 미리보기가 배치됩니다.
step1_basic_layout.py¶
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
from rename_engine import RenameEngine
class TwoPanelRenamer:
"""2-패널 레이아웃 기본 구조"""
def __init__(self):
self.root = tk.Tk()
self.root.title("KRenamer v5.0 - 2-패널 레이아웃")
self.root.geometry("1000x700")
self.root.minsize(800, 600)
# 엔진 초기화
self.engine = RenameEngine()
self.engine.on_files_changed = self.on_files_changed
self.engine.on_options_changed = self.on_options_changed
self.create_widgets()
self.create_variables()
self.bind_events()
def create_widgets(self):
"""위젯 생성"""
# 메인 컨테이너
main_frame = ttk.Frame(self.root, padding="5")
main_frame.pack(fill=tk.BOTH, expand=True)
# 상단 툴바
self.create_toolbar(main_frame)
# 2-패널 메인 영역
self.create_main_panels(main_frame)
# 하단 상태바
self.create_statusbar(main_frame)
def create_toolbar(self, parent):
"""상단 툴바"""
toolbar = ttk.Frame(parent)
toolbar.pack(fill=tk.X, pady=(0, 5))
# 파일 관리 버튼들
ttk.Button(toolbar, text="📁 파일 추가",
command=self.add_files).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(toolbar, text="📂 폴더 추가",
command=self.add_folder).pack(side=tk.LEFT, padx=(0, 5))
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
# 작업 버튼들
ttk.Button(toolbar, text="🔄 미리보기 새로고침",
command=self.refresh_preview).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(toolbar, text="✅ 이름 변경 실행",
command=self.execute_rename).pack(side=tk.LEFT, padx=(0, 5))
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=5)
# 초기화 버튼들
ttk.Button(toolbar, text="🗑️ 전체 삭제",
command=self.clear_all).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(toolbar, text="⚙️ 옵션 초기화",
command=self.reset_options).pack(side=tk.LEFT)
def create_main_panels(self, parent):
"""메인 2-패널 영역"""
# PanedWindow로 좌우 분할
paned = ttk.PanedWindow(parent, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
# 왼쪽 패널 (파일 관리 + 옵션)
left_panel = ttk.Frame(paned)
paned.add(left_panel, weight=1)
# 오른쪽 패널 (미리보기)
right_panel = ttk.Frame(paned)
paned.add(right_panel, weight=1)
self.create_left_panel(left_panel)
self.create_right_panel(right_panel)
def create_left_panel(self, parent):
"""왼쪽 패널 - 파일 관리와 옵션"""
# 파일 목록 섹션
files_group = ttk.LabelFrame(parent, text="파일 목록", padding="10")
files_group.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 파일 필터와 통계
filter_frame = ttk.Frame(files_group)
filter_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(filter_frame, text="필터:").pack(side=tk.LEFT)
self.filter_var = tk.StringVar(value="모든 파일")
filter_combo = ttk.Combobox(filter_frame, textvariable=self.filter_var,
values=["모든 파일", "이미지 파일", "문서 파일", "비디오 파일"],
width=12, state="readonly")
filter_combo.pack(side=tk.LEFT, padx=(5, 15))
self.file_count_var = tk.StringVar(value="파일 개수: 0")
ttk.Label(filter_frame, textvariable=self.file_count_var,
foreground="blue").pack(side=tk.RIGHT)
# 파일 목록 (Listbox)
list_frame = ttk.Frame(files_group)
list_frame.pack(fill=tk.BOTH, expand=True)
self.files_listbox = tk.Listbox(list_frame, selectmode=tk.EXTENDED,
font=("맑은 고딕", 9))
list_scroll = ttk.Scrollbar(list_frame, orient=tk.VERTICAL,
command=self.files_listbox.yview)
self.files_listbox.configure(yscrollcommand=list_scroll.set)
self.files_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
list_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# 파일 조작 버튼들
file_buttons = ttk.Frame(files_group)
file_buttons.pack(fill=tk.X, pady=(5, 0))
ttk.Button(file_buttons, text="❌ 선택 삭제",
command=self.remove_selected).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(file_buttons, text="🔺 위로",
command=self.move_up).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(file_buttons, text="🔻 아래로",
command=self.move_down).pack(side=tk.LEFT)
# 이름 변경 옵션 섹션
options_group = ttk.LabelFrame(parent, text="이름 변경 옵션", padding="10")
options_group.pack(fill=tk.X)
self.create_rename_options(options_group)
def create_rename_options(self, parent):
"""이름 변경 옵션 위젯들"""
# 접두사/접미사
prefix_frame = ttk.Frame(parent)
prefix_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(prefix_frame, text="접두사:", width=8).pack(side=tk.LEFT)
self.prefix_var = tk.StringVar()
ttk.Entry(prefix_frame, textvariable=self.prefix_var, width=20).pack(side=tk.LEFT, padx=(5, 15))
ttk.Label(prefix_frame, text="접미사:", width=8).pack(side=tk.LEFT)
self.suffix_var = tk.StringVar()
ttk.Entry(prefix_frame, textvariable=self.suffix_var, width=20).pack(side=tk.LEFT, padx=(5, 0))
# 찾기/바꾸기
replace_frame = ttk.Frame(parent)
replace_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(replace_frame, text="찾기:", width=8).pack(side=tk.LEFT)
self.find_var = tk.StringVar()
ttk.Entry(replace_frame, textvariable=self.find_var, width=20).pack(side=tk.LEFT, padx=(5, 15))
ttk.Label(replace_frame, text="바꾸기:", width=8).pack(side=tk.LEFT)
self.replace_var = tk.StringVar()
ttk.Entry(replace_frame, textvariable=self.replace_var, width=20).pack(side=tk.LEFT, padx=(5, 0))
# 순번 매기기
number_frame = ttk.Frame(parent)
number_frame.pack(fill=tk.X, pady=(0, 5))
self.use_numbering_var = tk.BooleanVar()
ttk.Checkbutton(number_frame, text="순번 매기기",
variable=self.use_numbering_var).pack(side=tk.LEFT)
ttk.Label(number_frame, text="시작:").pack(side=tk.LEFT, padx=(15, 5))
self.number_start_var = tk.IntVar(value=1)
ttk.Spinbox(number_frame, from_=1, to=999, width=5,
textvariable=self.number_start_var).pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(number_frame, text="자릿수:").pack(side=tk.LEFT, padx=(0, 5))
self.number_digits_var = tk.IntVar(value=3)
ttk.Spinbox(number_frame, from_=1, to=5, width=5,
textvariable=self.number_digits_var).pack(side=tk.LEFT)
def create_right_panel(self, parent):
"""오른쪽 패널 - 미리보기"""
preview_group = ttk.LabelFrame(parent, text="실시간 미리보기", padding="10")
preview_group.pack(fill=tk.BOTH, expand=True)
# 미리보기 테이블
tree_frame = ttk.Frame(preview_group)
tree_frame.pack(fill=tk.BOTH, expand=True)
columns = ("순번", "원본 파일명", "새 파일명", "상태")
self.preview_tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
# 컬럼 설정
self.preview_tree.heading("순번", text="#")
self.preview_tree.heading("원본 파일명", text="원본 파일명")
self.preview_tree.heading("새 파일명", text="새 파일명")
self.preview_tree.heading("상태", text="상태")
self.preview_tree.column("순번", width=50, anchor=tk.CENTER)
self.preview_tree.column("원본 파일명", width=200)
self.preview_tree.column("새 파일명", width=200)
self.preview_tree.column("상태", width=80, anchor=tk.CENTER)
# 스크롤바
tree_scroll = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL,
command=self.preview_tree.yview)
self.preview_tree.configure(yscrollcommand=tree_scroll.set)
self.preview_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# 미리보기 통계
stats_frame = ttk.Frame(preview_group)
stats_frame.pack(fill=tk.X, pady=(10, 0))
self.preview_stats_var = tk.StringVar(value="변경 예정: 0개, 오류: 0개")
ttk.Label(stats_frame, textvariable=self.preview_stats_var,
foreground="darkgreen").pack(side=tk.LEFT)
def create_statusbar(self, parent):
"""하단 상태바"""
statusbar = ttk.Frame(parent)
statusbar.pack(fill=tk.X, pady=(5, 0))
self.status_var = tk.StringVar(value="준비")
ttk.Label(statusbar, textvariable=self.status_var).pack(side=tk.LEFT)
# 파일 통계
self.file_stats_var = tk.StringVar(value="")
ttk.Label(statusbar, textvariable=self.file_stats_var).pack(side=tk.RIGHT)
def create_variables(self):
"""변수 초기화"""
# 초기 상태 설정
self.update_file_list()
self.update_preview()
def bind_events(self):
"""이벤트 바인딩"""
# 옵션 변경 감지
self.prefix_var.trace('w', self.on_option_change)
self.suffix_var.trace('w', self.on_option_change)
self.find_var.trace('w', self.on_option_change)
self.replace_var.trace('w', self.on_option_change)
self.use_numbering_var.trace('w', self.on_option_change)
self.number_start_var.trace('w', self.on_option_change)
self.number_digits_var.trace('w', self.on_option_change)
# 리스트박스 선택 변경
self.files_listbox.bind('<<ListboxSelect>>', self.on_file_select)
# 더블클릭으로 파일 열기
self.preview_tree.bind('<Double-1>', self.on_preview_double_click)
# 이벤트 핸들러들
def on_files_changed(self):
"""파일 목록 변경 시 호출"""
self.update_file_list()
self.update_preview()
self.update_statistics()
def on_options_changed(self):
"""옵션 변경 시 호출"""
self.update_preview()
def on_option_change(self, *args):
"""GUI 옵션 변경 시 엔진에 반영"""
self.engine.set_prefix(self.prefix_var.get())
self.engine.set_suffix(self.suffix_var.get())
self.engine.set_find_replace(self.find_var.get(), self.replace_var.get())
self.engine.set_numbering(
self.use_numbering_var.get(),
self.number_start_var.get(),
self.number_digits_var.get()
)
def on_file_select(self, event):
"""파일 선택 변경 시"""
selected = self.files_listbox.curselection()
if selected:
self.status_var.set(f"{len(selected)}개 파일 선택됨")
else:
self.status_var.set("준비")
def on_preview_double_click(self, event):
"""미리보기 더블클릭 시 파일 탐색기에서 열기"""
selection = self.preview_tree.selection()
if selection:
item = self.preview_tree.item(selection[0])
file_num = int(item['values'][0]) - 1
if 0 <= file_num < len(self.engine.files):
file_path = self.engine.files[file_num]
os.startfile(os.path.dirname(file_path))
# 파일 관리 메서드들
def add_files(self):
"""파일 추가"""
files = filedialog.askopenfilenames(
title="추가할 파일을 선택하세요",
filetypes=[
("모든 파일", "*.*"),
("이미지 파일", "*.jpg;*.jpeg;*.png;*.gif;*.bmp"),
("문서 파일", "*.txt;*.doc;*.docx;*.pdf"),
("비디오 파일", "*.mp4;*.avi;*.mkv;*.mov")
]
)
if files:
added = self.engine.add_files(list(files))
self.status_var.set(f"{added}개 파일이 추가되었습니다")
def add_folder(self):
"""폴더의 모든 파일 추가"""
folder = filedialog.askdirectory(title="폴더를 선택하세요")
if not folder:
return
try:
files = []
for item in os.listdir(folder):
item_path = os.path.join(folder, item)
if os.path.isfile(item_path):
files.append(item_path)
if files:
added = self.engine.add_files(files)
self.status_var.set(f"폴더에서 {added}개 파일이 추가되었습니다")
else:
messagebox.showinfo("정보", "선택한 폴더에 파일이 없습니다.")
except Exception as e:
messagebox.showerror("오류", f"폴더 읽기 실패: {str(e)}")
def remove_selected(self):
"""선택된 파일들 제거"""
selected = self.files_listbox.curselection()
if not selected:
messagebox.showwarning("경고", "제거할 파일을 선택하세요.")
return
removed = self.engine.remove_files_by_indices(list(selected))
self.status_var.set(f"{removed}개 파일이 제거되었습니다")
def move_up(self):
"""선택된 파일을 위로 이동"""
selected = self.files_listbox.curselection()
if not selected or selected[0] == 0:
return
# 단순 구현: 첫 번째 선택된 파일만 이동
index = selected[0]
file_path = self.engine.files.pop(index)
self.engine.files.insert(index - 1, file_path)
self.engine._notify_files_changed()
# 선택 상태 유지
self.files_listbox.selection_set(index - 1)
def move_down(self):
"""선택된 파일을 아래로 이동"""
selected = self.files_listbox.curselection()
if not selected or selected[0] >= len(self.engine.files) - 1:
return
index = selected[0]
file_path = self.engine.files.pop(index)
self.engine.files.insert(index + 1, file_path)
self.engine._notify_files_changed()
# 선택 상태 유지
self.files_listbox.selection_set(index + 1)
def clear_all(self):
"""모든 파일 삭제"""
if self.engine.get_file_count() > 0:
if messagebox.askyesno("확인", "모든 파일을 목록에서 제거하시겠습니까?"):
self.engine.clear_files()
self.status_var.set("모든 파일이 제거되었습니다")
def reset_options(self):
"""옵션 초기화"""
self.engine.reset_options()
# GUI 변수들도 초기화
self.prefix_var.set("")
self.suffix_var.set("")
self.find_var.set("")
self.replace_var.set("")
self.use_numbering_var.set(False)
self.number_start_var.set(1)
self.number_digits_var.set(3)
self.status_var.set("옵션이 초기화되었습니다")
# 미리보기 및 UI 업데이트
def update_file_list(self):
"""파일 목록 업데이트"""
self.files_listbox.delete(0, tk.END)
for file_path in self.engine.files:
filename = os.path.basename(file_path)
self.files_listbox.insert(tk.END, filename)
self.file_count_var.set(f"파일 개수: {self.engine.get_file_count()}")
def update_preview(self):
"""미리보기 업데이트"""
# 기존 항목 제거
for item in self.preview_tree.get_children():
self.preview_tree.delete(item)
if self.engine.get_file_count() == 0:
self.preview_tree.insert("", tk.END, values=(
"", "파일을 추가하세요", "", ""
))
self.preview_stats_var.set("변경 예정: 0개, 오류: 0개")
return
preview = self.engine.generate_preview()
valid_count = 0
error_count = 0
for i, (original, new, is_valid, error_msg) in enumerate(preview):
if is_valid:
status = "✅ 유효"
valid_count += 1
# 색상 태그 (나중에 추가)
tags = ("valid",)
else:
status = f"❌ {error_msg}"
error_count += 1
tags = ("error",)
self.preview_tree.insert("", tk.END, values=(
i + 1, original, new, status
), tags=tags)
# 색상 설정
self.preview_tree.tag_configure("valid", foreground="darkgreen")
self.preview_tree.tag_configure("error", foreground="red")
self.preview_stats_var.set(f"변경 예정: {valid_count}개, 오류: {error_count}개")
def update_statistics(self):
"""통계 정보 업데이트"""
stats = self.engine.get_statistics()
if stats['total_files'] > 0:
size_mb = stats['total_size'] / (1024 * 1024)
self.file_stats_var.set(
f"총 {stats['total_files']}개 파일, {size_mb:.1f}MB"
)
else:
self.file_stats_var.set("")
def refresh_preview(self):
"""미리보기 강제 새로고침"""
self.update_preview()
self.status_var.set("미리보기가 새로고침되었습니다")
def execute_rename(self):
"""파일명 변경 실행"""
if self.engine.get_file_count() == 0:
messagebox.showwarning("경고", "변경할 파일이 없습니다.")
return
# 미리보기에서 유효한 변경사항 확인
preview = self.engine.generate_preview()
valid_changes = sum(1 for _, _, is_valid, _ in preview if is_valid)
if valid_changes == 0:
messagebox.showwarning("경고", "변경할 수 있는 파일이 없습니다.")
return
# 확인 대화상자
if not messagebox.askyesno("확인",
f"{valid_changes}개 파일의 이름을 변경하시겠습니까?"):
return
# 실행
self.status_var.set("파일명 변경 중...")
self.root.update() # UI 즉시 업데이트
try:
results = self.engine.execute_rename()
# 결과 메시지
message = f"✅ 성공: {results['success']}개 파일 변경됨"
if results['failed'] > 0:
message += f"\n❌ 실패: {results['failed']}개 파일"
if results['errors']:
message += f"\n\n첫 번째 오류: {results['errors'][0]}"
messagebox.showinfo("작업 완료", message)
self.status_var.set(f"완료: {results['success']}개 파일 변경됨")
except Exception as e:
messagebox.showerror("오류", f"파일명 변경 중 오류 발생: {str(e)}")
self.status_var.set("오류 발생")
def run(self):
"""애플리케이션 실행"""
self.root.mainloop()
if __name__ == "__main__":
app = TwoPanelRenamer()
app.run()
실행 결과¶

Step 2에서는 완전한 2-패널 구조를 구현했습니다:
- 왼쪽 패널: 파일 목록, 필터링, 이름 변경 옵션
- 오른쪽 패널: 실시간 미리보기 테이블
- 상단 툴바: 자주 사용하는 기능들
- 하단 상태바: 현재 상태와 통계 정보
💡 Step 2에서 배운 2-패널 설계 원칙¶
- 공간 분할: PanedWindow로 유연한 화면 분할
- 기능별 그룹화: LabelFrame으로 관련 기능 묶기
- 정보 계층화: 툴바, 메인 영역, 상태바로 정보 구조화
- 실시간 동기화: 엔진과 GUI 간 즉시 데이터 반영
- 사용자 피드백: 모든 작업에 대한 명확한 상태 표시
⚡ Step 3: 실시간 미리보기 시스템¶
목표¶
Step 2의 기본 구조에 드래그 앤 드롭, 정규표현식 지원, 고급 필터링 등의 전문적인 기능들을 추가합니다.
step3_real_time_preview.py¶
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import re
try:
import tkinterdnd2
from tkinterdnd2 import DND_FILES, TkinterDnD
DND_AVAILABLE = True
except ImportError:
DND_AVAILABLE = False
print("경고: tkinterdnd2가 설치되지 않아 드래그 앤 드롭 기능을 사용할 수 없습니다.")
print("설치하려면: pip install tkinterdnd2")
from rename_engine import RenameEngine
class AdvancedRenamerGUI:
"""고급 실시간 미리보기 기능을 포함한 리네이머"""
def __init__(self):
if DND_AVAILABLE:
self.root = TkinterDnD.Tk()
else:
self.root = tk.Tk()
self.root.title("KRenamer v5.0 - 실시간 미리보기")
self.root.geometry("1200x800")
self.root.minsize(900, 600)
# 엔진 초기화
self.engine = RenameEngine()
self.engine.on_files_changed = self.on_files_changed
self.engine.on_options_changed = self.on_options_changed
# GUI 상태 변수들
self.search_text = tk.StringVar()
self.sort_column = "name" # name, size, type
self.sort_reverse = False
self.filter_mode = tk.StringVar(value="모든 파일")
self.create_widgets()
self.create_variables()
self.bind_events()
self.setup_drag_drop()
def create_widgets(self):
"""위젯 생성"""
# 메인 컨테이너
main_frame = ttk.Frame(self.root, padding="5")
main_frame.pack(fill=tk.BOTH, expand=True)
# 상단 툴바
self.create_toolbar(main_frame)
# 2-패널 메인 영역
self.create_main_panels(main_frame)
# 하단 상태바
self.create_statusbar(main_frame)
def create_toolbar(self, parent):
"""고급 툴바"""
toolbar = ttk.Frame(parent)
toolbar.pack(fill=tk.X, pady=(0, 5))
# 파일 관리 버튼들
file_group = ttk.LabelFrame(toolbar, text="파일 관리", padding="5")
file_group.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
ttk.Button(file_group, text="📁 파일 추가",
command=self.add_files, width=12).pack(side=tk.LEFT, padx=(0, 2))
ttk.Button(file_group, text="📂 폴더 추가",
command=self.add_folder, width=12).pack(side=tk.LEFT, padx=(0, 2))
ttk.Button(file_group, text="📋 클립보드",
command=self.add_from_clipboard, width=12).pack(side=tk.LEFT)
# 작업 버튼들
action_group = ttk.LabelFrame(toolbar, text="작업", padding="5")
action_group.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
ttk.Button(action_group, text="🔄 새로고침",
command=self.refresh_preview, width=10).pack(side=tk.LEFT, padx=(0, 2))
ttk.Button(action_group, text="✅ 실행",
command=self.execute_rename, width=10).pack(side=tk.LEFT, padx=(0, 2))
ttk.Button(action_group, text="💾 설정 저장",
command=self.save_settings, width=10).pack(side=tk.LEFT)
# 유틸리티 버튼들
util_group = ttk.LabelFrame(toolbar, text="유틸리티", padding="5")
util_group.pack(side=tk.RIGHT, fill=tk.Y)
ttk.Button(util_group, text="🗑️ 전체 삭제",
command=self.clear_all, width=10).pack(side=tk.LEFT, padx=(0, 2))
ttk.Button(util_group, text="⚙️ 초기화",
command=self.reset_options, width=10).pack(side=tk.LEFT)
def create_main_panels(self, parent):
"""메인 2-패널 영역"""
# PanedWindow로 좌우 분할
paned = ttk.PanedWindow(parent, orient=tk.HORIZONTAL)
paned.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
# 왼쪽 패널 (파일 관리 + 옵션)
left_panel = ttk.Frame(paned)
paned.add(left_panel, weight=1)
# 오른쪽 패널 (미리보기)
right_panel = ttk.Frame(paned)
paned.add(right_panel, weight=1)
self.create_left_panel(left_panel)
self.create_right_panel(right_panel)
def create_left_panel(self, parent):
"""왼쪽 패널 - 파일 관리와 옵션"""
# 파일 목록 섹션
files_group = ttk.LabelFrame(parent, text="파일 목록", padding="10")
files_group.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 검색과 필터
search_frame = ttk.Frame(files_group)
search_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(search_frame, text="검색:").pack(side=tk.LEFT)
search_entry = ttk.Entry(search_frame, textvariable=self.search_text, width=15)
search_entry.pack(side=tk.LEFT, padx=(5, 10))
ttk.Label(search_frame, text="필터:").pack(side=tk.LEFT)
filter_combo = ttk.Combobox(search_frame, textvariable=self.filter_mode,
values=["모든 파일", "이미지 파일", "문서 파일",
"비디오 파일", "오류 파일만"],
width=12, state="readonly")
filter_combo.pack(side=tk.LEFT, padx=(5, 10))
self.file_count_var = tk.StringVar(value="파일 개수: 0")
ttk.Label(search_frame, textvariable=self.file_count_var,
foreground="blue").pack(side=tk.RIGHT)
# 파일 목록 (Treeview로 업그레이드)
tree_frame = ttk.Frame(files_group)
tree_frame.pack(fill=tk.BOTH, expand=True)
# 파일 트리뷰
columns = ("크기", "형식", "경로")
self.files_tree = ttk.Treeview(tree_frame, columns=columns, show="tree headings")
# 컬럼 설정
self.files_tree.heading("#0", text="파일명", command=lambda: self.sort_files("name"))
self.files_tree.heading("크기", text="크기", command=lambda: self.sort_files("size"))
self.files_tree.heading("형식", text="형식", command=lambda: self.sort_files("type"))
self.files_tree.heading("경로", text="경로")
self.files_tree.column("#0", width=200)
self.files_tree.column("크기", width=80, anchor=tk.E)
self.files_tree.column("형식", width=60, anchor=tk.CENTER)
self.files_tree.column("경로", width=250)
# 스크롤바
tree_scroll = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL,
command=self.files_tree.yview)
self.files_tree.configure(yscrollcommand=tree_scroll.set)
self.files_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# 파일 조작 버튼들
file_buttons = ttk.Frame(files_group)
file_buttons.pack(fill=tk.X, pady=(5, 0))
ttk.Button(file_buttons, text="❌ 선택 삭제",
command=self.remove_selected, width=10).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(file_buttons, text="🔺 위로",
command=self.move_up, width=8).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(file_buttons, text="🔻 아래로",
command=self.move_down, width=8).pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(file_buttons, text="📋 경로 복사",
command=self.copy_selected_paths, width=10).pack(side=tk.RIGHT)
# 이름 변경 옵션 섹션
options_group = ttk.LabelFrame(parent, text="이름 변경 옵션", padding="10")
options_group.pack(fill=tk.X)
self.create_rename_options(options_group)
def create_rename_options(self, parent):
"""향상된 이름 변경 옵션 위젯들"""
# 접두사/접미사
prefix_frame = ttk.Frame(parent)
prefix_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(prefix_frame, text="접두사:", width=8).pack(side=tk.LEFT)
self.prefix_var = tk.StringVar()
ttk.Entry(prefix_frame, textvariable=self.prefix_var, width=15).pack(side=tk.LEFT, padx=(5, 15))
ttk.Label(prefix_frame, text="접미사:", width=8).pack(side=tk.LEFT)
self.suffix_var = tk.StringVar()
ttk.Entry(prefix_frame, textvariable=self.suffix_var, width=15).pack(side=tk.LEFT, padx=(5, 0))
# 찾기/바꾸기 (정규표현식 지원)
replace_frame = ttk.Frame(parent)
replace_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(replace_frame, text="찾기:", width=8).pack(side=tk.LEFT)
self.find_var = tk.StringVar()
find_entry = ttk.Entry(replace_frame, textvariable=self.find_var, width=15)
find_entry.pack(side=tk.LEFT, padx=(5, 15))
ttk.Label(replace_frame, text="바꾸기:", width=8).pack(side=tk.LEFT)
self.replace_var = tk.StringVar()
ttk.Entry(replace_frame, textvariable=self.replace_var, width=15).pack(side=tk.LEFT, padx=(5, 10))
self.use_regex_var = tk.BooleanVar()
ttk.Checkbutton(replace_frame, text="정규식",
variable=self.use_regex_var).pack(side=tk.LEFT)
# 순번 매기기
number_frame = ttk.Frame(parent)
number_frame.pack(fill=tk.X, pady=(0, 5))
self.use_numbering_var = tk.BooleanVar()
ttk.Checkbutton(number_frame, text="순번 매기기",
variable=self.use_numbering_var).pack(side=tk.LEFT)
ttk.Label(number_frame, text="시작:").pack(side=tk.LEFT, padx=(15, 5))
self.number_start_var = tk.IntVar(value=1)
ttk.Spinbox(number_frame, from_=1, to=9999, width=6,
textvariable=self.number_start_var).pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(number_frame, text="자릿수:").pack(side=tk.LEFT, padx=(0, 5))
self.number_digits_var = tk.IntVar(value=3)
ttk.Spinbox(number_frame, from_=1, to=6, width=4,
textvariable=self.number_digits_var).pack(side=tk.LEFT, padx=(0, 10))
ttk.Label(number_frame, text="구분자:").pack(side=tk.LEFT, padx=(0, 5))
self.number_separator_var = tk.StringVar(value="_")
separator_combo = ttk.Combobox(number_frame, textvariable=self.number_separator_var,
values=["_", "-", ".", " ", ""], width=4, state="readonly")
separator_combo.pack(side=tk.LEFT)
# 대소문자 변환
case_frame = ttk.Frame(parent)
case_frame.pack(fill=tk.X, pady=(0, 5))
ttk.Label(case_frame, text="대소문자:").pack(side=tk.LEFT)
self.case_mode_var = tk.StringVar(value="변경 안함")
case_combo = ttk.Combobox(case_frame, textvariable=self.case_mode_var,
values=["변경 안함", "소문자", "대문자", "첫글자만 대문자", "단어별 첫글자 대문자"],
width=15, state="readonly")
case_combo.pack(side=tk.LEFT, padx=(5, 0))
def create_right_panel(self, parent):
"""오른쪽 패널 - 고급 미리보기"""
preview_group = ttk.LabelFrame(parent, text="실시간 미리보기", padding="10")
preview_group.pack(fill=tk.BOTH, expand=True)
# 미리보기 컨트롤
control_frame = ttk.Frame(preview_group)
control_frame.pack(fill=tk.X, pady=(0, 5))
self.preview_mode_var = tk.StringVar(value="모두 보기")
mode_combo = ttk.Combobox(control_frame, textvariable=self.preview_mode_var,
values=["모두 보기", "변경된 파일만", "오류 파일만", "유효한 파일만"],
width=12, state="readonly")
mode_combo.pack(side=tk.LEFT, padx=(0, 10))
ttk.Button(control_frame, text="📄 결과 내보내기",
command=self.export_preview).pack(side=tk.RIGHT)
# 미리보기 테이블
tree_frame = ttk.Frame(preview_group)
tree_frame.pack(fill=tk.BOTH, expand=True)
columns = ("순번", "원본 파일명", "새 파일명", "변경사항", "상태")
self.preview_tree = ttk.Treeview(tree_frame, columns=columns, show="headings")
# 컬럼 설정
self.preview_tree.heading("순번", text="#")
self.preview_tree.heading("원본 파일명", text="원본 파일명")
self.preview_tree.heading("새 파일명", text="새 파일명")
self.preview_tree.heading("변경사항", text="변경사항")
self.preview_tree.heading("상태", text="상태")
self.preview_tree.column("순번", width=40, anchor=tk.CENTER)
self.preview_tree.column("원본 파일명", width=180)
self.preview_tree.column("새 파일명", width=180)
self.preview_tree.column("변경사항", width=100, anchor=tk.CENTER)
self.preview_tree.column("상태", width=80, anchor=tk.CENTER)
# 스크롤바
preview_scroll = ttk.Scrollbar(tree_frame, orient=tk.VERTICAL,
command=self.preview_tree.yview)
self.preview_tree.configure(yscrollcommand=preview_scroll.set)
self.preview_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
preview_scroll.pack(side=tk.RIGHT, fill=tk.Y)
# 미리보기 통계 및 진행 상황
stats_frame = ttk.Frame(preview_group)
stats_frame.pack(fill=tk.X, pady=(10, 0))
self.preview_stats_var = tk.StringVar(value="변경 예정: 0개, 오류: 0개")
ttk.Label(stats_frame, textvariable=self.preview_stats_var,
foreground="darkgreen").pack(side=tk.LEFT)
# 실시간 업데이트 진행바 (대용량 파일 처리시)
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(stats_frame, variable=self.progress_var,
mode='determinate', length=100)
# 필요시에만 표시
def create_statusbar(self, parent):
"""하단 상태바"""
statusbar = ttk.Frame(parent)
statusbar.pack(fill=tk.X, pady=(5, 0))
self.status_var = tk.StringVar(value="준비")
ttk.Label(statusbar, textvariable=self.status_var).pack(side=tk.LEFT)
# 파일 통계
self.file_stats_var = tk.StringVar(value="")
ttk.Label(statusbar, textvariable=self.file_stats_var).pack(side=tk.RIGHT)
# 드래그 앤 드롭 안내
if DND_AVAILABLE:
ttk.Label(statusbar, text="💡 파일을 여기로 드래그해서 추가할 수 있습니다.",
foreground="gray").pack(side=tk.LEFT, padx=(20, 0))
실행 결과¶

Step 3에서는 실시간 미리보기 시스템을 완성했습니다. 드래그 앤 드롭, 정규표현식 지원, 고급 필터링 등 전문적인 기능들이 추가되어 사용자 경험이 크게 향상되었습니다.
💡 Step 3에서 추가된 고급 기능들¶
- 드래그 앤 드롭: tkinterdnd2를 통한 파일 끌어서 놓기
- 정규표현식: 복잡한 패턴 매칭과 치환 지원
- 고급 필터링: 파일 형식, 오류 상태별 필터
- 검색 기능: 실시간 파일명 검색
- 정렬: 이름, 크기, 형식별 정렬
- 대소문자 변환: 다양한 케이스 변환 옵션
- 미리보기 내보내기: CSV/TXT 형태로 결과 저장
🎯 Step 4: 고급 UI 개선과 인터랙션¶
목표¶
전문적인 메뉴 시스템, 키보드 단축키, 설정 관리, 프리셋 시스템 등을 추가하여 완전한 전문 도구로 발전시킵니다.
step4_advanced_ui.py의 핵심 기능¶
# 메뉴 시스템
def create_menu_bar(self):
"""전문적인 메뉴 바"""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# 파일 메뉴
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="파일(F)", menu=file_menu, underline=0)
file_menu.add_command(label="파일 추가...", accelerator="Ctrl+O",
command=self.add_files, underline=0)
file_menu.add_command(label="폴더 추가...", accelerator="Ctrl+Shift+O",
command=self.add_folder, underline=0)
file_menu.add_separator()
file_menu.add_command(label="설정 저장...", accelerator="Ctrl+S",
command=self.save_settings, underline=0)
file_menu.add_command(label="설정 불러오기...", accelerator="Ctrl+L",
command=self.load_settings, underline=2)
file_menu.add_separator()
file_menu.add_command(label="종료", accelerator="Alt+F4",
command=self.quit_app, underline=0)
# 편집 메뉴
edit_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="편집(E)", menu=edit_menu, underline=0)
edit_menu.add_command(label="모두 선택", accelerator="Ctrl+A",
command=self.select_all_files, underline=2)
edit_menu.add_command(label="선택 해제", accelerator="Ctrl+D",
command=self.deselect_all_files, underline=0)
edit_menu.add_separator()
edit_menu.add_command(label="선택 파일 제거", accelerator="Delete",
command=self.remove_selected, underline=0)
edit_menu.add_command(label="모든 파일 제거", accelerator="Ctrl+Delete",
command=self.clear_all, underline=2)
# 도구 메뉴
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="도구(T)", menu=tools_menu, underline=0)
tools_menu.add_command(label="미리보기 새로고침", accelerator="F5",
command=self.refresh_preview, underline=0)
tools_menu.add_command(label="이름 변경 실행", accelerator="Ctrl+Enter",
command=self.execute_rename, underline=0)
tools_menu.add_separator()
tools_menu.add_command(label="설정 초기화", accelerator="Ctrl+R",
command=self.reset_options, underline=0)
tools_menu.add_command(label="프로그램 정보",
command=self.show_about, underline=0)
# 키보드 단축키
def bind_keyboard_shortcuts(self):
"""키보드 단축키 바인딩"""
# 파일 관리
self.root.bind('<Control-o>', lambda e: self.add_files())
self.root.bind('<Control-O>', lambda e: self.add_folder())
self.root.bind('<Delete>', lambda e: self.remove_selected())
self.root.bind('<Control-Delete>', lambda e: self.clear_all())
# 편집
self.root.bind('<Control-a>', lambda e: self.select_all_files())
self.root.bind('<Control-d>', lambda e: self.deselect_all_files())
# 작업
self.root.bind('<F5>', lambda e: self.refresh_preview())
self.root.bind('<Control-Return>', lambda e: self.execute_rename())
self.root.bind('<Control-r>', lambda e: self.reset_options())
# 설정
self.root.bind('<Control-s>', lambda e: self.save_settings())
self.root.bind('<Control-l>', lambda e: self.load_settings())
# 종료
self.root.bind('<Alt-F4>', lambda e: self.quit_app())
# ESC로 선택 해제
self.root.bind('<Escape>', lambda e: self.deselect_all_files())
# 설정 관리
def save_settings(self, filename=None):
"""현재 설정을 JSON 파일로 저장"""
if filename is None:
filename = filedialog.asksaveasfilename(
title="설정 저장",
defaultextension=".json",
filetypes=[("JSON 파일", "*.json"), ("모든 파일", "*.*")]
)
if not filename:
return
settings = {
'prefix': self.prefix_var.get(),
'suffix': self.suffix_var.get(),
'find_text': self.find_var.get(),
'replace_text': self.replace_var.get(),
'use_regex': self.use_regex_var.get(),
'use_numbering': self.use_numbering_var.get(),
'number_start': self.number_start_var.get(),
'number_digits': self.number_digits_var.get(),
'number_separator': self.number_separator_var.get(),
'case_mode': self.case_mode_var.get(),
'filter_mode': self.filter_mode.get(),
'preview_mode': self.preview_mode_var.get(),
'window_geometry': self.root.geometry(),
'created_time': datetime.now().isoformat()
}
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(settings, f, indent=2, ensure_ascii=False)
self.status_var.set(f"설정이 저장되었습니다: {os.path.basename(filename)}")
messagebox.showinfo("성공", f"설정이 저장되었습니다.\n{filename}")
except Exception as e:
messagebox.showerror("오류", f"설정 저장 실패: {str(e)}")
# 프리셋 시스템
def create_preset_panel(self, parent):
"""프리셋 관리 패널"""
preset_group = ttk.LabelFrame(parent, text="프리셋", padding="5")
preset_group.pack(fill=tk.X, pady=(5, 0))
preset_frame = ttk.Frame(preset_group)
preset_frame.pack(fill=tk.X)
self.preset_var = tk.StringVar()
self.preset_combo = ttk.Combobox(preset_frame, textvariable=self.preset_var,
width=20, state="readonly")
self.preset_combo.pack(side=tk.LEFT, padx=(0, 5))
ttk.Button(preset_frame, text="적용", width=6,
command=self.apply_preset).pack(side=tk.LEFT, padx=(0, 2))
ttk.Button(preset_frame, text="저장", width=6,
command=self.save_preset).pack(side=tk.LEFT, padx=(0, 2))
ttk.Button(preset_frame, text="삭제", width=6,
command=self.delete_preset).pack(side=tk.LEFT)
self.load_presets()
실행 결과¶

Step 4에서는 전문적인 메뉴 시스템과 키보드 단축키, 설정 관리 등을 추가했습니다. 이제 진정한 전문 도구다운 인터페이스를 갖추게 되었습니다.
💡 Step 4에서 추가된 전문 기능들¶
- 전문 메뉴 시스템: 접근성 높은 메뉴바와 컨텍스트 메뉴
- 키보드 단축키: 빠른 작업을 위한 단축키 지원
- 설정 관리: JSON 형태의 설정 저장/불러오기
- 프리셋 시스템: 자주 사용하는 설정 조합 저장
- 도움말 시스템: 키보드 단축키 및 정규식 가이드
- 내보내기/가져오기: 미리보기 결과를 다양한 형식으로 내보내기
- 고급 UI 패턴: 상태 표시, 진행률 표시, 툴팁
🏆 Step 5: 완성된 전문적 인터페이스¶
목표¶
멀티스레딩, 백업 시스템, 로깅, 배치 처리 등의 전문 기능을 추가하여 완전한 전문 도구로 완성합니다.
step5_professional.py의 핵심 아키텍처¶
# 백업 매니저
class BackupManager:
"""파일 백업 및 복구 관리"""
def __init__(self, backup_dir="backups"):
self.backup_dir = Path(backup_dir)
self.backup_dir.mkdir(exist_ok=True)
self.metadata_file = self.backup_dir / "backup_metadata.json"
self.load_metadata()
def create_backup(self, files_to_rename):
"""파일 변경 전 백업 생성"""
backup_id = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = self.backup_dir / f"backup_{backup_id}"
backup_path.mkdir(exist_ok=True)
backup_info = {
'id': backup_id,
'timestamp': datetime.now().isoformat(),
'files': [],
'backup_path': str(backup_path)
}
# 파일들을 백업 폴더로 복사
for original_path, new_name in files_to_rename:
if os.path.exists(original_path):
backup_file_path = backup_path / Path(original_path).name
shutil.copy2(original_path, backup_file_path)
backup_info['files'].append({
'original_path': original_path,
'original_name': Path(original_path).name,
'new_name': new_name,
'backup_file': str(backup_file_path)
})
self.metadata['backups'].append(backup_info)
self.save_metadata()
return backup_id
# 프로그레스 다이얼로그
class ProgressDialog:
"""진행 상황 표시 다이얼로그"""
def __init__(self, parent, title="작업 진행 중"):
self.parent = parent
self.dialog = tk.Toplevel(parent)
self.dialog.title(title)
self.dialog.geometry("400x150")
self.dialog.resizable(False, False)
self.dialog.transient(parent)
self.dialog.grab_set()
# 다이얼로그 중앙 배치
self.dialog.geometry("+%d+%d" %
(parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
self.cancelled = False
self.create_widgets()
def create_widgets(self):
main_frame = ttk.Frame(self.dialog, padding="20")
main_frame.pack(fill=tk.BOTH, expand=True)
# 현재 작업 표시
self.current_task_var = tk.StringVar(value="작업 준비 중...")
ttk.Label(main_frame, textvariable=self.current_task_var,
font=("맑은 고딕", 10)).pack(pady=(0, 10))
# 진행률 표시
self.progress_var = tk.DoubleVar()
self.progress_bar = ttk.Progressbar(main_frame, variable=self.progress_var,
mode='determinate', length=350)
self.progress_bar.pack(pady=(0, 10))
# 진행률 텍스트
self.progress_text_var = tk.StringVar(value="0 / 0 (0%)")
ttk.Label(main_frame, textvariable=self.progress_text_var).pack(pady=(0, 10))
# 취소 버튼
ttk.Button(main_frame, text="취소", command=self.cancel).pack()
def update_progress(self, current, total, task_name=""):
"""진행 상황 업데이트"""
if total > 0:
progress = (current / total) * 100
self.progress_var.set(progress)
self.progress_text_var.set(f"{current} / {total} ({progress:.1f}%)")
if task_name:
self.current_task_var.set(task_name)
self.dialog.update()
return not self.cancelled
# 멀티스레딩 실행
def execute_rename_threaded(self):
"""별도 스레드에서 파일명 변경 실행"""
if self.engine.get_file_count() == 0:
messagebox.showwarning("경고", "변경할 파일이 없습니다.")
return
# 백업 생성 여부 확인
if self.create_backup_var.get():
if not messagebox.askyesno("백업 확인",
"파일 변경 전에 백업을 생성하시겠습니까?\n" +
"(권장: 실수로 인한 데이터 손실 방지)"):
return
# 진행 다이얼로그 표시
progress_dialog = ProgressDialog(self.root, "파일명 변경 중")
# 별도 스레드에서 실행
def rename_thread():
try:
preview = self.engine.generate_preview()
valid_changes = [(i, original, new) for i, (original, new, is_valid, _)
in enumerate(preview) if is_valid]
if not valid_changes:
self.root.after(0, lambda: messagebox.showwarning("경고", "변경할 수 있는 파일이 없습니다."))
return
# 백업 생성
backup_id = None
if self.create_backup_var.get():
files_to_backup = [(self.engine.files[i], new) for i, original, new in valid_changes]
backup_id = self.backup_manager.create_backup(files_to_backup)
results = {'success': 0, 'failed': 0, 'errors': [], 'renamed_files': []}
for idx, (i, original_name, new_name) in enumerate(valid_changes):
if progress_dialog.cancelled:
break
# 진행 상황 업데이트
if not progress_dialog.update_progress(idx + 1, len(valid_changes),
f"처리 중: {original_name}"):
break
original_path = self.engine.files[i]
directory = os.path.dirname(original_path)
new_path = os.path.join(directory, new_name)
try:
if original_name != new_name: # 실제 변경이 필요한 경우만
os.rename(original_path, new_path)
self.engine.files[i] = new_path
results['success'] += 1
results['renamed_files'].append((original_path, new_path))
# 로그 기록
self.logger.info(f"파일명 변경 성공: {original_name} -> {new_name}")
except Exception as e:
results['failed'] += 1
results['errors'].append(f"{original_name}: {str(e)}")
self.logger.error(f"파일명 변경 실패: {original_name} -> {new_name}, 오류: {str(e)}")
time.sleep(0.01) # UI 응답성 확보
# UI 업데이트는 메인 스레드에서
self.root.after(0, lambda: self.on_rename_completed(results, backup_id, progress_dialog))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("오류", f"파일명 변경 중 예기치 못한 오류: {str(e)}"))
self.logger.error(f"파일명 변경 중 예외 발생: {str(e)}")
finally:
self.root.after(0, progress_dialog.close)
# 스레드 시작
threading.Thread(target=rename_thread, daemon=True).start()
# 로깅 시스템
def setup_logging(self):
"""로깅 시스템 초기화"""
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
log_filename = log_dir / f"krenamer_{datetime.now().strftime('%Y%m%d')}.log"
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_filename, encoding='utf-8'),
logging.StreamHandler() # 콘솔 출력도 유지
]
)
self.logger = logging.getLogger(__name__)
self.logger.info("KRenamer 시작")
# 시작 시 시스템 정보 로그
self.logger.info(f"Python 버전: {sys.version}")
self.logger.info(f"tkinter DnD 사용 가능: {DND_AVAILABLE}")
self.logger.info(f"작업 디렉토리: {os.getcwd()}")
실행 결과¶

Step 5에서는 멀티스레딩, 백업 시스템, 진행 상황 표시, 로깅 등 완전한 전문 도구의 기능을 모두 갖추게 되었습니다. 이제 대용량 파일도 안전하게 처리할 수 있는 완성된 KRenamer입니다.
💡 Step 5의 전문 기능들¶
- 멀티스레딩: UI 블로킹 없는 대용량 파일 처리
- 백업 시스템: 자동 백업 생성과 복구 기능
- 진행 상황 표시: 실시간 진행률과 취소 기능
- 로깅 시스템: 상세한 작업 기록과 오류 추적
- 배치 처리: 폴더 단위 대량 처리 지원
- 중복 검사: 파일명 충돌 자동 감지
- 복구 시스템: 백업을 통한 원상 복구
- 설정 마이그레이션: 버전 간 설정 호환성
📚 이번 챕터에서 배운 것들¶
핵심 아키텍처 패턴¶
- MVC 패턴: Model(Engine) - View(GUI) - Controller(Event Handlers) 분리
- Observer 패턴: 콜백을 통한 상태 변경 알림
- Command 패턴: 사용자 작업을 명령 객체로 캡슐화
- Strategy 패턴: 다양한 이름 변경 전략 지원
2-패널 레이아웃의 장점¶
- 효율적 공간 활용: 좌우 분할로 더 많은 정보 표시
- 작업 흐름 개선: 입력 → 미리보기 → 실행의 자연스러운 흐름
- 동시 비교: 원본과 결과를 동시에 확인 가능
- 전문적 외관: 복잡한 도구에 적합한 인터페이스
실시간 미리보기의 핵심¶
- 즉시성: 옵션 변경 시 지연 없는 결과 표시
- 정확성: 실제 실행 결과와 100% 일치하는 미리보기
- 안전성: 문제가 있는 변경사항 사전 경고
- 직관성: 색상과 아이콘으로 상태를 명확히 표현
Chapter 5 완료!
2-패널 레이아웃과 실시간 미리보기가 완성되었습니다! 이제 전문적인 파일 리네이머의 핵심 구조가 완성되었습니다.
연습 과제
- 파일 크기별 필터링 기능 추가하기
- 미리보기 결과를 CSV로 내보내기
- 키보드 단축키 지원 추가하기
- 설정을 JSON 파일로 저장/불러오기