import os import shutil import json import threading import tkinter as tk from tkinter import filedialog, scrolledtext CONFIG_PATH = os.path.expanduser("~/Library/Application Support/PhotoSelector/config.json") RAW_EXTENSIONS = ('.pef', '.arw', '.cr2', '.cr3', '.nef', '.dng', '.orf', '.rw2', '.raf') def load_config(): try: with open(CONFIG_PATH, "r", encoding="utf-8") as f: return json.load(f) except Exception: return {} def save_config(data): os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) with open(CONFIG_PATH, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) class PhotoSelectorApp: def __init__(self, root): self.root = root root.title("사진 선별기") root.geometry("640x480") root.resizable(True, True) config = load_config() self.jpg_dir = tk.StringVar(value=config.get("jpg_dir", "")) self.raw_dir = tk.StringVar(value=config.get("raw_dir", "")) self.target_dir = tk.StringVar(value=config.get("target_dir", "")) self._build_row("1. 고른 JPG 폴더", self.jpg_dir, 0) self._build_row("2. RAW(메모리카드) 폴더", self.raw_dir, 1) self._build_row("3. 결과를 저장할 폴더", self.target_dir, 2) self.run_button = tk.Button(root, text="실행", command=self.run_clicked, height=2) self.run_button.grid(row=3, column=0, columnspan=3, sticky="ew", padx=10, pady=10) self.log = scrolledtext.ScrolledText(root, state="disabled", wrap="word") self.log.grid(row=4, column=0, columnspan=3, sticky="nsew", padx=10, pady=(0, 10)) root.grid_columnconfigure(1, weight=1) root.grid_rowconfigure(4, weight=1) def _build_row(self, label_text, var, row): tk.Label(self.root, text=label_text, anchor="w", width=20).grid( row=row, column=0, sticky="w", padx=10, pady=5 ) entry = tk.Entry(self.root, textvariable=var) entry.grid(row=row, column=1, sticky="ew", padx=5, pady=5) tk.Button(self.root, text="폴더 선택", command=lambda: self._choose_dir(var)).grid( row=row, column=2, sticky="e", padx=10, pady=5 ) def _choose_dir(self, var): initial = var.get() or os.path.expanduser("~") chosen = filedialog.askdirectory(initialdir=initial) if chosen: var.set(chosen) def log_write(self, text): self.log.configure(state="normal") self.log.insert("end", text + "\n") self.log.see("end") self.log.configure(state="disabled") def run_clicked(self): jpg_dir = self.jpg_dir.get().strip() raw_dir = self.raw_dir.get().strip() target_dir = self.target_dir.get().strip() if not jpg_dir or not raw_dir or not target_dir: self.log_write("❌ 세 개의 폴더를 모두 선택해주세요.") return save_config({"jpg_dir": jpg_dir, "raw_dir": raw_dir, "target_dir": target_dir}) self.run_button.configure(state="disabled") self.log.configure(state="normal") self.log.delete("1.0", "end") self.log.configure(state="disabled") thread = threading.Thread( target=self._run_task, args=(jpg_dir, raw_dir, target_dir), daemon=True ) thread.start() def _run_task(self, jpg_dir, raw_dir, target_dir): try: self._process(jpg_dir, raw_dir, target_dir) finally: self.root.after(0, lambda: self.run_button.configure(state="normal")) def _process(self, jpg_dir, raw_dir, target_dir): log = lambda text: self.root.after(0, self.log_write, text) if not os.path.exists(target_dir): os.makedirs(target_dir) try: jpg_files = os.listdir(jpg_dir) except FileNotFoundError: log(f"❌ 오류: JPG 폴더 경로를 찾을 수 없습니다: {jpg_dir}") return jpg_names = { os.path.splitext(f)[0] for f in jpg_files if f.lower().endswith((".jpg", ".jpeg")) } log(f"🔍 JPG 폴더에서 총 {len(jpg_names)}개의 사진 이름을 확인했습니다.") try: raw_files = os.listdir(raw_dir) except FileNotFoundError: log(f"❌ 오류: RAW(메모리카드) 폴더 경로를 찾을 수 없습니다: {raw_dir}") return count = 0 for f in raw_files: name, ext = os.path.splitext(f) if ext.lower() in RAW_EXTENSIONS and name in jpg_names: src_path = os.path.join(raw_dir, f) dst_path = os.path.join(target_dir, f) shutil.copy(src_path, dst_path) count += 1 log(f"[{count}] 매칭 성공 복사 중: {f}") log("-" * 50) log(f"🎉 작업 완료! 총 {count}개의 RAW 파일을 '{target_dir}' 폴더로 모았습니다.") log("-" * 50) def main(): root = tk.Tk() PhotoSelectorApp(root) root.mainloop() if __name__ == "__main__": main()