Project

사진 이어 붙이기 - Tkinter

Python Developer 2024. 12. 1. 20:14

프로그램 사용 영상

목차

  1. 프로젝트 설명
  2. 개발 환경
  3. 주요 기능
  4. 주요 메서드
  5. 프로젝트 코드 및 주석 설명

1. 프로젝트 설명

이미지 파일들을 사용자 선택 옵션에 따라 하나의 이미지로 병합하는 프로그램

2. 개발 환경

Anaconda, Python 3.12.4, Jupyter Notebook

2. 주요 기능

  1. 옵션
    • 너비 설정(원본 유지, 1024, 800, 640): 병합할 이미지의 너비를 설정
    • 간격 설정(좁게, 보통, 넓게): 병합할 이미지들간의 간격 설정
    • 포맷(png, jpg): 저장할 이미지 포맷 설정
    • 정렬(수직, 수평): 병합할 이미지의 방향 설정
  2. 파일관리
    • 파일 추가 기능
    • 파일 삭제 기능
    • 파일 순서 변경 기능
    • 파일 리스트 확인 기능
  3. 미리보기: 파일 리스트에 선택된 이미지를 보여주는 기능
  4. 저장경로 선택 기능
    • 찾아보기: 다이얼로그로 저장 경로 선택
  5. 이미지 병합 진행상태를 프로그레스바로 볼 수 있음
  6. 시작 및 닫기: 이미지 병합을 실행 및 프로그램 종료
  7. 저장 결과 화면: 병합된 이미지를 저장결과 화면에 보여줌
  8. 줌: 병합된 결과 이미지를 확대/축소 가능

3. 주요 메서드

  • __init__(root): GUI 초기화 및 기본 설정
  • on_drop(): 드래그 앤 드롭, 파일을 마우스로 옮겨서 리스트에 추가
  • save_config(): 현재 사용자 설정을 config.json 파일에 저장
  • load_config(): 저장된 사용자 설정 불러옴
  • create_*_frame(): GUI 구성(옵션, 파일 관리, 리스트 박스 img 미리보기, 저장 경로, 진행 상황, 실행, 저장결과 보기)
  • add_file(): 파일 추가 다이얼로그를 열어 선택된 이미지를 리스트에 추가
  • del_file(): 선택된 파일 삭제
  • move_up()/move_down(): 리스트에서 선택된 파일 순서 변경
  • browse_dest_path(): 저장 경로 설정
  • start(): 파일유효성 검사 및 병합 작업 실행
  • merge_image(): 이미지 병합 실행
    • update_progress_bar(): 진행바 업데이트
    • create_result_image(): 병합된 이미지 결과 생성
    • loading_window = Toplevel(self.root): 이미지 병합 실행시 이미지 저장을 대기하는 모달창 생성
  • update_preview(): 선택된 이미지, 결과 이미지 미리보기 뷰에 업데이트
  • zoom(): 저장 결과 미리보기 확대/축소 기능

4. 사용된 라이브러리

  • tkinter: GUI 생성
  • tkinterdnd2: 드래그 앤 드롭 지원

pip install tkinterdnd2==0.4.2

  • pillow(PIL): 이미지 처리

pip install pillow==10.3.0

  • json



import os
from tkinter import *
from tkinter import filedialog, messagebox, ttk
from tkinterdnd2 import TkinterDnD, DND_FILES
from PIL import Image, ImageTk
import json


class ImageMergerApp:
    def __init__(self, root):
        self.root = root # main gui 생성
        self.root.title("YongSeokHa Project - Image Merge Program")  # gui 제목 설정

        self.window_width, self.window_height = 1500, 900  # gui 가로, 세로 길이
        self.root.geometry(f"{self.window_width}x{self.window_height}")  # gui 크기 설정

        # 드래그 앤 드롭 초기화 (파일을 드래그해서 App 으로 옮기는 기능)
        self.root.drop_target_register(DND_FILES)  # 파일 드래그 앤 드롭을 위한 설정
        self.root.dnd_bind('<<Drop>>', self.on_drop)  # 파일이 드롭될 때 호출될 함수 지정

        self.list_image_preview = None  # 리스트 박스에 img파일 미리보기 저장 변수 초기화
        self.result_image_preview = None  # 저장 결과 view 프레임에 img 변수 초기화

        #self.last_opened_dir = os.path.expanduser("~")  # 기본 열기 경로를 사용자 홈 디렉토리로 설정
        self.last_opened_dir = os.path.expanduser("../Media/images/")  # 파일추가 기본 열기 디렉토리 경로 설정
        self.result_opened_dir = os.path.expanduser("../Media/images/")  # 저장결과 기본 열기 디렉토리 경로 설정
        self.config_file = "config.json"  # 사용자 설정 저장 파일

        # 왼쪽 프레임, 오른쪽 프레임 분할
        self.left_frame = Frame(self.root)
        self.right_frame = Frame(self.root)

        self.left_frame.pack(side="left", fill="both", expand=True)
        self.right_frame.pack(side="left", fill="both", expand=True)

        # 내부 위젯 크기가 프레임 크기를 변경하지 않도록 설정
        self.left_frame.pack_propagate(False)
        self.right_frame.pack_propagate(False)

        # 각 프레임 가로 길이 설정
        self.left_frame.config(width=self.window_width / 2)  # 왼쪽 프레임 너비 설정
        self.right_frame.config(width=self.window_width / 2)  # 오른쪽 프레임 너비 설정

        # 왼쪽 프레임: 옵션, 파일 관리, 리스트 박스 img 미리보기, 저장 경로, 진행 상황, 실행 버튼
        self.create_option_frame()
        self.create_file_frame()
        self.create_preview_frame()
        self.create_path_frame()
        self.create_progress_frame()
        self.create_run_frame()

        # 오른쪽 프레임: 저장 결과 img 보기
        self.create_result_preview_frame()

        # 사용자 설정 로드
        self.load_config()

    def on_drop(self, event):
        """드래그 앤 드롭 이벤트 처리"""
        files = self.root.tk.splitlist(event.data)  # 드롭된 파일 목록 가져오기
        for file in files:
            if file.lower().endswith(('.png', '.jpg', '.jpeg')):  # 이미지 파일만 추가
                self.list_file.insert(END, file)  # 파일 목록에 추가

    def load_config(self):
        """사용자 설정 값 불러오기"""
        with open(self.config_file, 'r') as f:
            config = json.load(f)  # json 파일 읽기
            self.cmb_width.set(config.get("width", "원본유지"))  # 넓이
            self.cmb_space.set(config.get("space", "없음"))  # 간격
            self.cmb_format.set(config.get("format", "PNG"))  # img 포맷
            self.cmb_align.set(config.get("align", "수직"))  # 정렬 방식

    def save_config(self):
        """사용자 설정 값 저장"""
        config = {
            "width": self.cmb_width.get(),  # 가로넓이
            "space": self.cmb_space.get(),  # 간격
            "format": self.cmb_format.get(),  # img 포맷
            "align": self.cmb_align.get()  # 정렬 방식
        }
        with open(self.config_file, 'w') as f:
            json.dump(config, f)  # 설정 파일로 저장

    def create_file_frame(self):
        """파일 추가/삭제 및 순서 변경 프레임 생성"""
        frame = LabelFrame(self.left_frame, text="파일 관리")  # 파일 관리 프레임
        frame.pack(fill="both", padx=5, pady=5, expand=True)

        list_frame = Frame(frame)  # 파일 목록 프레임
        list_frame.pack(side="left", fill="both", expand=True, padx=5)

        scrollbar = Scrollbar(list_frame)  # 스크롤바 생성
        scrollbar.pack(side="right", fill="y")

        self.list_file = Listbox(
            list_frame, selectmode="extended", height=10, yscrollcommand=scrollbar.set  # 파일 목록 리스트박스
        )
        self.list_file.pack(side="left", fill="both", expand=True)
        self.list_file.bind("<<ListboxSelect>>", lambda event: self.update_preview("listbox_preview"))  # 항목 선택 시 미리보기 업데이트

        scrollbar.config(command=self.list_file.yview)  # 스크롤바와 리스트박스 연결

        button_frame = Frame(frame)  # 버튼 프레임
        button_frame.pack(side="right", fill="y", padx=5)

        Button(button_frame, text="파일 추가", command=self.add_file).pack(fill="x", pady=2)  # 파일 추가 버튼
        Button(button_frame, text="선택 삭제", command=self.del_file).pack(fill="x", pady=2)  # 선택 삭제 버튼

        Button(button_frame, text="↑", command=self.move_up, width=3, height=1).pack(padx=(0, 70), pady=(80, 10))  # 위로 이동 버튼
        Button(button_frame, text="↓", command=self.move_down, width=3, height=1).pack(padx=(0, 70))  # 아래로 이동 버튼

    def create_preview_frame(self):
        """미리보기 프레임 생성"""
        frame = LabelFrame(self.left_frame, text="미리보기")  # 미리보기 프레임
        frame.pack(fill="both", padx=5, pady=5, expand=True)

        self.lbl_listfile_preview = Label(frame, text="이미지를 선택하세요", anchor="center")  # 미리보기 텍스트 라벨
        self.lbl_listfile_preview.pack(fill="both", expand=True, padx=5, pady=5)

    def create_result_preview_frame(self):
        """저장 결과 미리보기 프레임 생성"""
        frame = LabelFrame(self.right_frame, text="저장 결과 화면", bg="lightgray")  # 결과 미리보기 프레임
        frame.pack(side='top', fill="both", padx=5, pady=5, expand=True)

        # Canvas 생성 및 추가
        self.canvas = Canvas(frame, bg="white")  # 캔버스 생성
        self.canvas.pack(side="top", fill="both", expand=True)

        # 스크롤바 추가
        self.x_scroll = Scrollbar(self.canvas, orient=HORIZONTAL, command=self.canvas.xview)  # 가로 스크롤바
        self.x_scroll.pack(side="bottom", fill="x")
        self.y_scroll = Scrollbar(self.canvas, orient=VERTICAL, command=self.canvas.yview)  # 세로 스크롤바
        self.y_scroll.pack(side="right", fill="y")

        # Canvas와 스크롤바 연결
        self.canvas.config(xscrollcommand=self.x_scroll.set, yscrollcommand=self.y_scroll.set)

        # 확대/축소 슬라이더 추가
        self.scale_preview = Scale(frame, from_=0, to=400, orient="horizontal", label="Zoom (%)", command=self.zoom)  # 슬라이더 생성
        self.scale_preview.set(100)  # 기본값 100%
        self.scale_preview.pack(side="top", fill="x", padx=5, pady=5)

    def create_option_frame(self):
        """옵션 선택 프레임 생성"""
        frame = LabelFrame(self.left_frame, text="옵션")  # 옵션 설정 프레임
        frame.pack(fill="x", padx=5, pady=5)

        self.label_width = Label(frame, text="가로넓이", width=10)  # 넓이 라벨
        self.label_width.pack(side="left", padx=5, pady=5)

        self.cmb_width = ttk.Combobox(frame, state="readonly", values=["원본유지", "1024", "800", "640"], width=10)  # 넓이 옵션 콤보박스 생성
        self.cmb_width.current(0)  # 기본값 설정
        self.cmb_width.pack(side="left", padx=5, pady=5)

        Label(frame, text="간격", width=10).pack(side="left", padx=5, pady=5)  # 간격 라벨
        self.cmb_space = ttk.Combobox(frame, state="readonly", values=["없음", "좁게", "보통", "넓게"], width=10)  # 간격 옵션 콤보박스 생성
        self.cmb_space.current(0)
        self.cmb_space.pack(side="left", padx=5, pady=5)

        Label(frame, text="포맷", width=10).pack(side="left", padx=5, pady=5)  # 포맷 라벨
        self.cmb_format = ttk.Combobox(frame, state="readonly", values=["PNG", "JPG"], width=10)  # 포맷 옵션 콤보박스 생성
        self.cmb_format.current(0)
        self.cmb_format.pack(side="left", padx=5, pady=5)

        Label(frame, text="정렬", width=8).pack(side="left", padx=5, pady=5)  # 정렬 라벨  # 정렬 옵션 리스트
        self.cmb_align = ttk.Combobox(frame, state="readonly", values=["수직", "수평"], width=10)  # 콤보박스 생성
        self.cmb_align.current(0)
        self.cmb_align.pack(side="left", padx=5, pady=5)

        self.update_label() # 정렬 옵션이 수직/수평 에 따라 넓이 라벨 가로/세로 변경
        self.cmb_align.bind("<<ComboboxSelected>>", self.update_label) # 콤보 박스 선택에 따라 update_label 합수 호출

    def create_path_frame(self):
        """저장 경로 선택 프레임 생성"""
        frame = LabelFrame(self.left_frame, text="저장 경로")  # 저장 경로 프레임
        frame.pack(fill="x", padx=5, pady=5)

        self.txt_dest_path = Entry(frame)  # 경로 입력 필드 생성
        self.txt_dest_path.pack(side="left", fill="x", expand=True, padx=5, pady=5)

        Button(frame, text="찾아보기", command=self.browse_dest_path).pack(side="right", padx=5, pady=5)  # 찾아보기 버튼

    def create_progress_frame(self):
        """진행 상황 표시 프레임 생성"""
        frame = LabelFrame(self.left_frame, text="진행 상황")  # 진행 상황 프레임
        frame.pack(fill="x", padx=5, pady=5)

        self.p_var = DoubleVar()  # 진행 상황 변수 생성
        self.progress_bar = ttk.Progressbar(frame, maximum=100, variable=self.p_var)  # 진행바 생성
        self.progress_bar.pack(fill="x", padx=5, pady=5)

        self.progress_label = Label(frame, text="") # 진행률 표시
        self.progress_label.pack(pady=10)

    def create_run_frame(self):
        """실행 버튼 프레임 생성"""
        frame = Frame(self.left_frame)  # 실행 버튼 프레임
        frame.pack(fill="x", padx=5, pady=5)

        Button(frame, text="닫기", command=self.root.destroy).pack(side="right", padx=5, pady=5)  # 닫기 버튼
        Button(frame, text="시작", command=self.start).pack(side="right", padx=5, pady=5)  # 시작 버튼

    def update_label(self, event=None):
        """정렬 옵션 선택에 따라 가로넓이/세로넓이 변경"""
        # 콤보박스에서 선택된 값 가져오기
        selected_align = self.cmb_align.get()

        # 선택 값에 따라 라벨 텍스트 변경
        if selected_align == "수평":
            self.label_width.config(text="세로넓이")
        else:
            self.label_width.config(text="가로넓이")

    def add_file(self):
        """파일 추가"""
        # 파일 추가하기 위한 다이얼로그 열기
        files = filedialog.askopenfilenames(
            title="이미지 파일을 선택하세요",  # 다이얼로그 제목
            filetypes=(("PNG 파일", "*.png"), ("JPG 파일", "*.jpg"), ("모든 파일", "*.*")),  # 파일 유형 필터
            initialdir=self.last_opened_dir,  # 마지막으로 열었던 디렉토리로 시작
        )
        if files:  # 파일이 선택되었으면
            self.last_opened_dir = os.path.dirname(files[0])  # 마지막 열었던 디렉토리 갱신
        for file in files:  # 선택된 각 파일을 목록에 추가
            self.list_file.insert(END, file)

    def del_file(self):
        """파일 삭제"""
        # 선택된 파일들을 삭제
        for index in reversed(self.list_file.curselection()):  # 선택된 항목을 역순으로 삭제
            self.list_file.delete(index)
        self.update_preview()  # 미리보기 업데이트

    def move_up(self):
        """파일 순서 위로 이동"""
        # 선택된 파일들을 위로 이동
        indices = self.list_file.curselection()  # 선택된 인덱스 목록
        for i in indices:
            if i > 0:  # 첫 번째 항목이 아니면
                text = self.list_file.get(i)  # 항목 내용 가져오기
                self.list_file.delete(i)  # 항목 삭제
                self.list_file.insert(i - 1, text)  # 위로 이동하여 재삽입
                self.list_file.selection_set(i - 1)  # 새 위치를 선택 상태로 설정
        self.update_preview()  # 미리보기 업데이트

    def move_down(self):
        """파일 순서 아래로 이동"""
        # 선택된 파일들을 아래로 이동
        indices = self.list_file.curselection()  # 선택된 인덱스 목록
        for i in reversed(indices):  # 역순으로 이동
            if i < self.list_file.size() - 1:  # 마지막 항목이 아니면
                text = self.list_file.get(i)  # 항목 내용 가져오기
                self.list_file.delete(i)  # 항목 삭제
                self.list_file.insert(i + 1, text)  # 아래로 이동하여 재삽입
                self.list_file.selection_set(i + 1)  # 새 위치를 선택 상태로 설정
        self.update_preview()  # 미리보기 업데이트

    def browse_dest_path(self):
        """저장 경로 선택"""
        # 저장 경로를 선택하는 다이얼로그 열기
        folder_selected = filedialog.askdirectory(
            title="저장 경로를 선택하세요",  # 다이얼로그 제목
            initialdir=self.result_opened_dir  # 마지막으로 열었던 디렉토리로 시작
        )
        if folder_selected:  # 폴더가 선택되었으면
            self.txt_dest_path.delete(0, END)  # 경로 입력란 초기화
            self.txt_dest_path.insert(0, folder_selected)  # 경로 입력란에 선택된 폴더 경로 삽입
            self.result_opened_dir = folder_selected  # 마지막 열었던 디렉토리 갱신

    def check_file_and_path(self):
        """유효성 검사"""
        if not self.list_file.size():  # 파일 목록이 비어 있으면
            messagebox.showwarning("경고", "이미지 파일을 추가하세요")
            return False
        if not self.txt_dest_path.get():  # 저장 경로가 비어 있으면
            messagebox.showwarning("경고", "저장 경로를 선택하세요")
            return False
        return True

    def start(self):
        """유효성 검사 및 이미지 병합 실행"""
        self.save_config()
        if not self.check_file_and_path():  # 파일과 경로 확인
            return
        self.merge_image()  # 이미지 병합 실행
        self.scale_preview.set(100) # 줌 값 초기화


    def merge_image(self):
        """이미지 합치기"""
        try:
            # 사용자 설정 값 가져오기
            img_width = -1 if self.cmb_width.get() == "원본유지" else int(self.cmb_width.get())  # 넓이 설정
            img_space = {"없음": 0, "좁게": 30, "보통": 60, "넓게": 90}[self.cmb_space.get()]  # 간격 설정
            img_format = self.cmb_format.get().lower()  # 포맷 설정 (소문자)
            align = self.cmb_align.get()  # "정렬 설정

            # 이미지 설정
            images = [Image.open(x) for x in self.list_file.get(0, END)]  # 리스트 박스 이미지 목록 가져오기
            resized_images = [self.resize_and_center_image(img, img_width, align) for img in images]  # 이미지 크기 조정

            # 파일 저장 
            self.result_img_file_path = filedialog.asksaveasfilename(
                title="파일 저장",  # 다이얼로그 제목
                defaultextension=f".{img_format}",  # 기본 확장자 설정
                filetypes=(  # 파일 유형 설정
                    ("모든 파일", "*.*"),
                    ("PNG 파일", "*.png"),
                    ("JPG 파일", "*.jpg"),
                ),
            )

            # 이미지 결과
            result_img = self.create_result_image(resized_images, align, img_space)  # 이미지 병합 처리

            # 로딩 창 설정 (모달로 동작)
            loading_window = Toplevel(self.root)  # 로딩 창 생성
            loading_window.title("저장 대기중")  # 로딩 창 제목
            Label(loading_window, text="파일 이름을 설정하고 저장이 완료되면 자동으로 닫힙니다. \n기다려 주세요..!").pack(padx=20, pady=20)  # 라벨 표시

            # 로딩 창을 모달로 설정
            loading_window.transient(self.root)  # loading_window 창은 root 창 위에 고정
            loading_window.grab_set()  # loading_window가 닫히기 전까지는 root 및 다른 창과의 상호작용이 제한
            loading_window.focus_set()  # 로딩 창에 포커스 설정
            loading_window.update()   # 로딩 창이 생성되자마자 즉시 렌더링되도록 보장
            # update()가 없으면 로딩 창이 잠깐 멈추거나 렌더링이 지연될 수 있음

            if self.result_img_file_path: # 파일 경로 설정 확인
                # 결과 저장
                result_img.save(self.result_img_file_path)

                # img 결과 저장 후 미리보기 view 업데이트
                self.update_preview(place='result_preview')

                # 로딩 창 닫기
                loading_window.destroy()

                # 프로그레스바, 결과 img 저장을 기다리는 시간 있어서 한칸 남겨 놓고 저장 되면 100% 채움
                self.update_progress_bar(len(resized_images), len(resized_images))

                messagebox.showinfo("알림", "작업이 완료되었습니다!")  # 완료 메시지 표시

            else:
                loading_window.destroy()  # 로딩 창 닫기
                messagebox.showinfo("알림", "저장이 취소되었습니다.")  # 취소 메시지 표시
        except Exception as e:
            messagebox.showerror("에러", f"이미지 병합 중 오류가 발생했습니다.\n{e}")  # 에러 메시지 표시

    def resize_and_center_image(self, img, img_width, align):
        """이미지를 비율 유지하며 크기 조정하고 흰색 배경에 배치."""
        try:
            if img_width > -1:  # 너비가 지정된 경우
                # 비율에 맞춰 크기 변경
                if align == "수직":
                    new_size = (int(img_width), int(img_width * img.size[1] / img.size[0]))

                elif align == "수평":
                    new_size = (int(img_width * img.size[0] / img.size[1]), int(img_width))

            else:  # 원본유지
                new_size = (int(img.size[0]), int(img.size[1]))

            resized_img = img.resize(new_size, Image.Resampling.LANCZOS)  # img 크기 조정
            canvas = Image.new("RGB", resized_img.size, (255, 255, 255))  # 흰색 배경의 새로운 캔버스 생성
            canvas.paste(resized_img)  # 이미지를 캔버스에 붙여넣기
        except Exception as e:
            raise ValueError(f"resize_and_center_image - 이미지 조정 과정 에러 : {e}") from e # 이미지 조정 에러 처리
        return canvas  # 캔버스를 반환

    def create_result_image(self, resized_images, align, img_space):
        """이미지를 정렬 방향에 따라 병합."""
        try:
            if align == "수직":  # 수직 정렬인 경우
                max_width = max(img.size[0] for img in resized_images)  # 최대 너비 계산
                total_height = sum(img.size[1] for img in resized_images) + img_space * (len(resized_images) - 1)  # 총 높이 계산
                result_img = Image.new("RGB", (max_width, total_height), (255, 255, 255))  # 새로운 이미지를 생성
                offset = 0  # 시작 오프셋 설정
                for idx, img in enumerate(resized_images):  # 각 이미지를 수직으로 배치
                    result_img.paste(img, (0, offset))
                    offset += img.size[1] + img_space  # 다음 이미지의 오프셋 계산
                    self.update_progress_bar(idx, len(resized_images))

            elif align == "수평":  # 수평 정렬인 경우
                total_width = sum(img.size[0] for img in resized_images) + img_space * (len(resized_images) - 1)  # 총 너비 계산
                max_height = max(img.size[1] for img in resized_images)  # 최대 높이 계산
                result_img = Image.new("RGB", (total_width, max_height), (255, 255, 255))
                offset = 0
                for idx, img in enumerate(resized_images):  # 각 이미지를 수평으로 배치
                    result_img.paste(img, (offset, 0))
                    offset += img.size[0] + img_space
                    self.update_progress_bar(idx, len(resized_images))
        except Exception as e:
            raise ValueError(f"create_result_image - 이미지 병합 과정 에러 : {e}") from e # 이미지 병합 에러 처리

        return result_img  # 병합된 결과 이미지 반환

    def update_progress_bar(self, current, total):
        """진행 상태를 업데이트하고, 텍스트로 진행 상황을 표시."""
        progress = (current / total) * 100
        self.p_var.set(progress)  # 진행률 % 설정
        self.progress_bar.update()  # 진행률 업데이트

        self.progress_label.config(text=f"{int(progress)}% 완료") # 진행률 라벨로 표시시

    def selected_listbox_image_path(self):
        """리스트 박스 선택된 이미지 경로 가져오기"""
        selected = self.list_file.curselection()  # 리스트박스에서 선택된 항목 가져오기
        if not selected:  # 선택된 항목이 없으면
            self.lbl_listfile_preview.config(image='', text='이미지를 선택하세요')  # 선택 메시지 표시
            return
        list_image_filepath = self.list_file.get(selected[0])  # 선택된 파일 경로 가져오기
        return list_image_filepath  # 파일 경로 반환

    def update_preview(self, place='listbox_preview'):
        """미리보기 업데이트"""
        if place == 'listbox_preview':  # 리스트박스 미리보기
            try:
                list_image_filepath = self.selected_listbox_image_path()  # 이미지 파일 경로 가져오기
                if list_image_filepath is None: # listbox에서 파일삭제시 미리보기 return
                    return
                img = Image.open(list_image_filepath)  # 이미지 열기
                img.thumbnail((self.window_width / 3, self.window_height / 3))  # 미리보기 크기 조정
                self.list_image_preview = ImageTk.PhotoImage(img)  # Tkinter 이미지 객체로 변환print("lbl_listfile_preview
                self.lbl_listfile_preview.config(image=self.list_image_preview, text="")  # 미리보기 업데이트
            except Exception as e:
                self.lbl_listfile_preview.config(text=f"listbox_preview 오류: {str(e)}", image="")  # 오류 처리

        elif place == 'result_preview':  # 결과 미리보기
            try:
                self.img = Image.open(self.result_img_file_path)  # 결과 이미지 열기

                # 캔버스 넓이 가져오기
                canvas_width = self.canvas.winfo_width()
                canvas_height = self.canvas.winfo_height()

                self.img.thumbnail((canvas_width, canvas_height))  # 미리보기 크기 조정
                #if self.canvas_image_id is None:  # 저장 결과 이미지를 새로 로딩할 때
                self.result_image_preview = ImageTk.PhotoImage(self.img)  # Tkinter 이미지 객체로 변환

                # Canvas 중앙 좌표 계산
                x_center = canvas_width // 2
                y_center = canvas_height // 2

                # 이미지 중앙에 배치
                self.canvas_image_id = self.canvas.create_image(x_center, y_center, image=self.result_image_preview, anchor="center")

            except Exception as e: # 에러시 canvas 로 저장 결과 preview 에 표출
                self.canvas.create_text(
                    self.x_center,
                    self.y_center,
                    text="에러 발생: 파일을 불러올 수 없습니다.",
                    fill="red",
                    font=("Arial", 16),
                    anchor="center"
                )

    def zoom(self, event=None):  # event=None 추가
        """확대/축소 비율 계산 및 이미지 업데이트"""
        scale = self.scale_preview.get() / 100  # 확대/축소 비율 계산
        if not hasattr(self, "img"):  # 이미지가 없으면
            return

        # 원본 이미지를 기준으로 크기 변경
        width, height = int(self.img.width * scale), int(self.img.height * scale)  # 새로운 크기 계산
        resized_image = self.img.resize((width, height), Image.Resampling.LANCZOS)  # 이미지 크기 조정

        # ImageTk.PhotoImage 객체로 변환
        self.result_image_preview = ImageTk.PhotoImage(resized_image)

        # Canvas의 중심 좌표 계산
        canvas_width = max(self.canvas.winfo_width(), width)  # 캔버스 크기 조정
        canvas_height = max(self.canvas.winfo_height(), height)
        x_center = canvas_width // 2 # 캔버스 중앙값 산
        y_center = canvas_height // 2

        # Canvas에 이미지 업데이트
        self.canvas.delete("all")  # 기존의 모든 항목 삭제
        self.canvas.create_image(x_center, y_center, image=self.result_image_preview, anchor="center")  # 새로운 이미지 배치

        # Scrollregion 설정 (캔버스의 스크롤 범위 설정)
        self.canvas.configure(scrollregion=(0, 0, width, height))  # 캔버스의 전체 영역을 스크롤 범위로 설정

if __name__ == "__main__":
    root = TkinterDnD.Tk()  # 드래그 앤 드롭 가능한 Tkinter 루트 창 생성
    app = ImageMergerApp(root)  # 앱 인스턴스 생성
    root.mainloop()  # Tkinter 이벤트 루프 실행