본문 바로가기

개발 이야기/기타

Claude.ai 무료 버전을 이용한 맥용 사진관리 프로그램 만들기

320x100

안녕하세요? 엘카입니다.

Claude.ai 성능 테스트 삼아 맥북에서 사진관리 프로그램을 만들었습니다.

 

제 요구사항을 작성해서 요청해 봤습니다.

클로드가 역시 개발에 참 좋은 친구 같아서 자주 사용하는 데, 무료 버전도 훌륭합니다.

 


 

 

사진 관리 프로그램이군요! 맥북 M2 Max에서 사용할 사진 관리 도구를 만들어드릴게요.

언어 추천: macOS에서는 Swift + SwiftUI가 가장 좋은 선택입니다. 네이티브 성능과 macOS 통합성이 뛰어나고, 파일 시스템 접근과 이미지 처리가 최적화되어 있어요.

하지만 더 빠른 개발을 위해 Python + Tkinter 또는 Electron + React도 고려할 수 있습니다. 일단 Python으로 프로토타입을 만들어보겠습니다.

핵심 기능들:

  • 좌측: 폴더 트리 네비게이션
  • 우측: 이미지 그리드 뷰 + 풀스크린 뷰어
  • 파일 복사 시 날짜별 폴더 자동 생성
  • JPG/RAW 파일 분리 저장
  • 이미지 뷰어 (스페이스바로 다음, X키로 삭제)
  • JPG-RAW 동기화 삭제

몇 가지 확인사항:

  1. RAW 파일 확장자는 어떤 것들인가요? (.CR2, .NEF, .ARW 등)
  2. 날짜 기반 폴더 형식은 어떻게 하실건가요? (YYYY-MM-DD, YYYY/MM 등)

일단 기본 구조로 Python 기반 프로그램을 만들어보겠습니다!완성했습니다! 사진 관리 프로그램의 주요 기능들:

🎯 핵심 기능

  • 좌측 폴더 네비게이션: 폴더 구조를 트리뷰로 탐색
  • 우측 이미지 그리드: 선택한 폴더의 JPG 이미지들을 썸네일로 표시
  • 풀스크린 이미지 뷰어: 이미지 더블클릭으로 전체 화면 뷰어 실행
  • 날짜별 자동 폴더 생성: 파일 수정 날짜 기준으로 YYYY-MM-DD 형식
  • JPG/RAW 파일 분리: JPG는 루트에, RAW는 raw 하위 폴더에 저장

⌨️ 키보드 단축키 (이미지 뷰어에서)

  • 스페이스바: 다음 이미지로 이동
  • X키: 현재 이미지 삭제 (동일한 이름의 RAW 파일도 함께 삭제)
  • ESC키: 이미지 뷰어 닫기

🛠 설치 및 실행

# 필요한 라이브러리 설치
pip install pillow

# 프로그램 실행
python photo_manager.py

💡 사용법

  1. 상단에서 소스 경로(SD카드)와 목적지 경로 설정
  2. "사진 복사 시작" 버튼으로 일괄 복사
  3. 좌측에서 폴더 선택하여 이미지 탐색
  4. 이미지 더블클릭으로 뷰어 실행
  5. 뷰어에서 스페이스바로 넘기면서 X키로 불필요한 사진 삭제

지원하는 RAW 형식은 CR2, NEF, ARW, DNG, RAF 등 주요 카메라 브랜드들을 포함했어요.

더 개선하고 싶은 부분이 있거나 다른 기능이 필요하시면 말씀해 주세요! 🚀

 

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from tkinter import Canvas, Scrollbar
import os
import shutil
from datetime import datetime
from PIL import Image, ImageTk
import glob
from pathlib import Path

class PhotoManager:
    def __init__(self, root):
        self.root = root
        self.root.title("Photo Manager - 사진 관리 도구")
        self.root.geometry("1200x800")
        
        # 지원하는 파일 형식
        self.jpg_extensions = ['.jpg', '.jpeg', '.JPG', '.JPEG']
        self.raw_extensions = ['.cr2', '.CR2', '.nef', '.NEF', '.arw', '.ARW', '.dng', '.DNG', '.raf', '.RAF']
        
        # 현재 선택된 경로들
        self.source_path = ""
        self.destination_path = ""
        self.current_folder = ""
        self.current_images = []
        self.current_image_index = 0
        self.viewer_mode = False
        
        self.setup_ui()
        
    def setup_ui(self):
        # 메인 프레임 설정
        main_frame = ttk.Frame(self.root)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 상단 경로 설정 프레임
        path_frame = ttk.LabelFrame(main_frame, text="경로 설정", padding=10)
        path_frame.pack(fill=tk.X, pady=(0, 10))
        
        # 소스 경로 설정
        ttk.Label(path_frame, text="소스 경로 (SD카드/외장디스크):").grid(row=0, column=0, sticky=tk.W, pady=2)
        self.source_entry = ttk.Entry(path_frame, width=60)
        self.source_entry.grid(row=0, column=1, padx=(10, 5), pady=2)
        ttk.Button(path_frame, text="선택", command=self.select_source_path).grid(row=0, column=2, pady=2)
        
        # 목적지 경로 설정
        ttk.Label(path_frame, text="목적지 경로:").grid(row=1, column=0, sticky=tk.W, pady=2)
        self.dest_entry = ttk.Entry(path_frame, width=60)
        self.dest_entry.grid(row=1, column=1, padx=(10, 5), pady=2)
        ttk.Button(path_frame, text="선택", command=self.select_dest_path).grid(row=1, column=2, pady=2)
        
        # 복사 버튼
        ttk.Button(path_frame, text="사진 복사 시작", command=self.copy_photos).grid(row=2, column=1, pady=10)
        
        # 하단 메인 컨텐츠 프레임
        content_frame = ttk.Frame(main_frame)
        content_frame.pack(fill=tk.BOTH, expand=True)
        
        # 좌측 폴더 트리
        left_frame = ttk.LabelFrame(content_frame, text="폴더 네비게이션", padding=5)
        left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 5))
        
        self.folder_tree = ttk.Treeview(left_frame, width=25)
        self.folder_tree.pack(fill=tk.BOTH, expand=True)
        self.folder_tree.bind("<<TreeviewSelect>>", self.on_folder_select)
        
        # 우측 이미지 뷰
        self.right_frame = ttk.LabelFrame(content_frame, text="이미지 뷰", padding=5)
        self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
        
        # 이미지 리스트 프레임 (스크롤 가능)
        self.image_canvas = Canvas(self.right_frame)
        self.image_scrollbar = Scrollbar(self.right_frame, orient="vertical", command=self.image_canvas.yview)
        self.image_canvas.configure(yscrollcommand=self.image_scrollbar.set)
        
        self.image_frame = ttk.Frame(self.image_canvas)
        self.image_canvas.create_window((0, 0), window=self.image_frame, anchor="nw")
        
        self.image_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.image_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 이미지 뷰어 프레임 (처음에는 숨김)
        self.viewer_frame = ttk.Frame(self.right_frame)
        self.viewer_label = ttk.Label(self.viewer_frame)
        self.viewer_label.pack(expand=True)
        
        # 키보드 이벤트 바인딩
        self.root.bind('<KeyPress>', self.on_key_press)
        self.root.focus_set()
        
        # 초기 폴더 로드
        self.load_initial_folders()
    
    def select_source_path(self):
        path = filedialog.askdirectory(title="소스 폴더 선택")
        if path:
            self.source_path = path
            self.source_entry.delete(0, tk.END)
            self.source_entry.insert(0, path)
            self.load_folder_tree(path)
    
    def select_dest_path(self):
        path = filedialog.askdirectory(title="목적지 폴더 선택")
        if path:
            self.destination_path = path
            self.dest_entry.delete(0, tk.END)
            self.dest_entry.insert(0, path)
    
    def load_initial_folders(self):
        # 홈 디렉토리 로드
        home_path = os.path.expanduser("~")
        self.load_folder_tree(home_path)
    
    def load_folder_tree(self, root_path):
        # 트리 초기화
        for item in self.folder_tree.get_children():
            self.folder_tree.delete(item)
        
        # 루트 노드 추가
        root_node = self.folder_tree.insert("", "end", text=os.path.basename(root_path), 
                                           values=[root_path], open=True)
        
        # 하위 폴더 로드
        self.load_subfolders(root_node, root_path)
    
    def load_subfolders(self, parent_node, path):
        try:
            for item in os.listdir(path):
                item_path = os.path.join(path, item)
                if os.path.isdir(item_path) and not item.startswith('.'):
                    node = self.folder_tree.insert(parent_node, "end", text=item, 
                                                 values=[item_path])
                    # 하위 폴더가 있는지 확인
                    try:
                        if any(os.path.isdir(os.path.join(item_path, x)) for x in os.listdir(item_path) if not x.startswith('.')):
                            self.folder_tree.insert(node, "end", text="...")  # 더미 노드
                    except PermissionError:
                        pass
        except PermissionError:
            pass
    
    def on_folder_select(self, event):
        selection = self.folder_tree.selection()
        if selection:
            item = selection[0]
            path = self.folder_tree.item(item, "values")[0]
            self.current_folder = path
            self.load_images(path)
            
            # 하위 폴더 동적 로딩
            children = self.folder_tree.get_children(item)
            if len(children) == 1 and self.folder_tree.item(children[0], "text") == "...":
                self.folder_tree.delete(children[0])
                self.load_subfolders(item, path)
    
    def load_images(self, folder_path):
        # 이미지 프레임 초기화
        for widget in self.image_frame.winfo_children():
            widget.destroy()
        
        # 이미지 파일 찾기
        image_files = []
        for ext in self.jpg_extensions:
            image_files.extend(glob.glob(os.path.join(folder_path, f"*{ext}")))
        
        self.current_images = sorted(image_files)
        
        # 이미지 썸네일 생성 및 표시
        row, col = 0, 0
        max_cols = 4
        
        for i, image_path in enumerate(self.current_images):
            try:
                # 썸네일 생성
                img = Image.open(image_path)
                img.thumbnail((150, 150))
                photo = ImageTk.PhotoImage(img)
                
                # 이미지 라벨 생성
                img_label = ttk.Label(self.image_frame, image=photo)
                img_label.image = photo  # 참조 유지
                img_label.grid(row=row, column=col, padx=5, pady=5)
                
                # 더블클릭 이벤트 바인딩
                img_label.bind("<Double-Button-1>", lambda e, idx=i: self.open_image_viewer(idx))
                
                # 파일명 표시
                filename_label = ttk.Label(self.image_frame, text=os.path.basename(image_path))
                filename_label.grid(row=row+1, column=col, padx=5, pady=(0, 10))
                
                col += 1
                if col >= max_cols:
                    col = 0
                    row += 2
                    
            except Exception as e:
                print(f"이미지 로드 실패: {image_path}, 오류: {e}")
        
        # 스크롤 영역 업데이트
        self.image_frame.update_idletasks()
        self.image_canvas.configure(scrollregion=self.image_canvas.bbox("all"))
    
    def open_image_viewer(self, image_index):
        self.current_image_index = image_index
        self.viewer_mode = True
        self.show_image_in_viewer()
        
        # 이미지 뷰어 프레임 표시
        self.image_canvas.pack_forget()
        self.image_scrollbar.pack_forget()
        self.viewer_frame.pack(fill=tk.BOTH, expand=True)
        
        self.right_frame.configure(text=f"이미지 뷰어 - {os.path.basename(self.current_images[image_index])}")
    
    def show_image_in_viewer(self):
        if self.current_images and 0 <= self.current_image_index < len(self.current_images):
            try:
                image_path = self.current_images[self.current_image_index]
                img = Image.open(image_path)
                
                # 화면 크기에 맞게 조정
                viewer_width = self.viewer_frame.winfo_width()
                viewer_height = self.viewer_frame.winfo_height()
                
                if viewer_width > 1 and viewer_height > 1:
                    img.thumbnail((viewer_width - 20, viewer_height - 20))
                
                photo = ImageTk.PhotoImage(img)
                self.viewer_label.configure(image=photo)
                self.viewer_label.image = photo
                
            except Exception as e:
                messagebox.showerror("오류", f"이미지 표시 실패: {e}")
    
    def close_image_viewer(self):
        self.viewer_mode = False
        self.viewer_frame.pack_forget()
        self.image_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        self.image_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.right_frame.configure(text="이미지 뷰")
    
    def on_key_press(self, event):
        if self.viewer_mode:
            if event.keysym == 'space':
                # 다음 이미지
                if self.current_image_index < len(self.current_images) - 1:
                    self.current_image_index += 1
                    self.show_image_in_viewer()
                    self.right_frame.configure(text=f"이미지 뷰어 - {os.path.basename(self.current_images[self.current_image_index])}")
            
            elif event.keysym == 'x':
                # 현재 이미지 삭제
                self.delete_current_image()
            
            elif event.keysym == 'Escape':
                # 뷰어 닫기
                self.close_image_viewer()
    
    def delete_current_image(self):
        if not self.current_images or self.current_image_index >= len(self.current_images):
            return
        
        current_image = self.current_images[self.current_image_index]
        
        # 확인 대화상자
        if messagebox.askyesno("삭제 확인", f"이미지를 삭제하시겠습니까?\n{os.path.basename(current_image)}"):
            try:
                # JPG 파일 삭제
                os.remove(current_image)
                
                # 동일한 이름의 RAW 파일 찾아서 삭제
                base_name = os.path.splitext(os.path.basename(current_image))[0]
                raw_folder = os.path.join(os.path.dirname(current_image), "raw")
                
                if os.path.exists(raw_folder):
                    for raw_ext in self.raw_extensions:
                        raw_file = os.path.join(raw_folder, f"{base_name}{raw_ext}")
                        if os.path.exists(raw_file):
                            os.remove(raw_file)
                            print(f"RAW 파일 삭제: {raw_file}")
                
                # 현재 이미지 리스트에서 제거
                self.current_images.pop(self.current_image_index)
                
                # 다음 이미지 표시 또는 뷰어 종료
                if self.current_images:
                    if self.current_image_index >= len(self.current_images):
                        self.current_image_index = len(self.current_images) - 1
                    self.show_image_in_viewer()
                else:
                    self.close_image_viewer()
                    
                messagebox.showinfo("삭제 완료", "이미지가 삭제되었습니다.")
                
            except Exception as e:
                messagebox.showerror("삭제 실패", f"이미지 삭제 중 오류 발생: {e}")
    
    def copy_photos(self):
        if not self.source_path or not self.destination_path:
            messagebox.showerror("오류", "소스 경로와 목적지 경로를 모두 설정해주세요.")
            return
        
        try:
            # 모든 이미지 파일 찾기
            jpg_files = []
            raw_files = []
            
            for root, dirs, files in os.walk(self.source_path):
                for file in files:
                    file_path = os.path.join(root, file)
                    _, ext = os.path.splitext(file)
                    
                    if ext.lower() in [e.lower() for e in self.jpg_extensions]:
                        jpg_files.append(file_path)
                    elif ext.lower() in [e.lower() for e in self.raw_extensions]:
                        raw_files.append(file_path)
            
            if not jpg_files and not raw_files:
                messagebox.showinfo("알림", "복사할 이미지 파일이 없습니다.")
                return
            
            # 날짜별 폴더 생성 및 파일 복사
            copied_count = 0
            
            for jpg_file in jpg_files:
                # 파일 수정 날짜 기준으로 폴더 생성
                file_time = os.path.getmtime(jpg_file)
                date_str = datetime.fromtimestamp(file_time).strftime("%Y-%m-%d")
                
                # 목적지 폴더 생성
                date_folder = os.path.join(self.destination_path, date_str)
                os.makedirs(date_folder, exist_ok=True)
                
                # JPG 파일 복사
                dest_file = os.path.join(date_folder, os.path.basename(jpg_file))
                shutil.copy2(jpg_file, dest_file)
                copied_count += 1
            
            for raw_file in raw_files:
                # 파일 수정 날짜 기준으로 폴더 생성
                file_time = os.path.getmtime(raw_file)  
                date_str = datetime.fromtimestamp(file_time).strftime("%Y-%m-%d")
                
                # RAW 폴더 생성
                date_folder = os.path.join(self.destination_path, date_str)
                raw_folder = os.path.join(date_folder, "raw")
                os.makedirs(raw_folder, exist_ok=True)
                
                # RAW 파일 복사
                dest_file = os.path.join(raw_folder, os.path.basename(raw_file))
                shutil.copy2(raw_file, dest_file)
                copied_count += 1
            
            messagebox.showinfo("복사 완료", f"총 {copied_count}개의 파일이 복사되었습니다.")
            
        except Exception as e:
            messagebox.showerror("복사 실패", f"파일 복사 중 오류 발생: {e}")

def main():
    root = tk.Tk()
    app = PhotoManager(root)
    root.mainloop()

if __name__ == "__main__":
    main()

 

 

반응형