From fac2e65ed8ea19f626b20051afe38705cde14735 Mon Sep 17 00:00:00 2001 From: gahusb Date: Fri, 24 Apr 2026 09:02:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(travel-proxy):=20indexer.py=20=E2=80=94=20?= =?UTF-8?q?=ED=8F=B4=EB=8D=94=20=EB=8F=99=EA=B8=B0=ED=99=94=20+=20?= =?UTF-8?q?=EC=8D=B8=EB=84=A4=EC=9D=BC=20=EC=9D=BC=EA=B4=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- travel-proxy/app/indexer.py | 125 ++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 travel-proxy/app/indexer.py diff --git a/travel-proxy/app/indexer.py b/travel-proxy/app/indexer.py new file mode 100644 index 0000000..ec116fd --- /dev/null +++ b/travel-proxy/app/indexer.py @@ -0,0 +1,125 @@ +import os +import time +import json +import logging +from pathlib import Path +from typing import Any, Dict, List, Set + +from PIL import Image + +from . import db + +logger = logging.getLogger(__name__) + +IMAGE_EXT = {".jpg", ".jpeg", ".png", ".webp"} +THUMB_SIZE = (480, 480) + + +def _scan_folder(folder: Path) -> List[Dict[str, Any]]: + """폴더 내 이미지 파일 목록 수집 (os.scandir).""" + if not folder.exists(): + return [] + items = [] + with os.scandir(folder) as entries: + for entry in entries: + if entry.is_file() and Path(entry.name).suffix.lower() in IMAGE_EXT: + items.append({ + "filename": entry.name, + "mtime": entry.stat().st_mtime, + }) + return items + + +def _generate_thumb(src: Path, dest: Path) -> bool: + """원본에서 480x480 썸네일 생성. 성공 시 True.""" + dest.parent.mkdir(parents=True, exist_ok=True) + tmp = dest.with_name(dest.stem + ".tmp" + dest.suffix) + try: + with Image.open(src) as im: + im.thumbnail(THUMB_SIZE) + ext = dest.suffix.lower() + if ext in (".jpg", ".jpeg"): + fmt = "JPEG" + elif ext == ".png": + fmt = "PNG" + elif ext == ".webp": + fmt = "WEBP" + else: + fmt = (im.format or "").upper() or "JPEG" + im.save(tmp, format=fmt, quality=85, optimize=True) + tmp.replace(dest) + return True + except Exception as e: + logger.warning("Thumb generation failed: %s → %s", src, e) + try: + if tmp.exists(): + tmp.unlink() + except Exception: + pass + return False + + +def sync( + travel_root: Path, + thumb_root: Path, + region_map_path: Path, +) -> Dict[str, Any]: + """ + 폴더 스캔 → DB 동기화 + 썸네일 일괄 생성. + + Returns: + {"added": int, "removed": int, "thumbs_generated": int, "duration_sec": float} + """ + start = time.time() + + # 1. region_map.json에서 전체 앨범 폴더 수집 + with open(region_map_path, "r", encoding="utf-8") as f: + region_map = json.load(f) + + all_albums: Set[str] = set() + for v in region_map.values(): + if isinstance(v, list): + all_albums.update(v) + elif isinstance(v, dict) and isinstance(v.get("albums"), list): + all_albums.update(v["albums"]) + + # 2. 각 앨범 폴더 스캔 → DB 동기화 + added = 0 + removed = 0 + + for album in sorted(all_albums): + folder = travel_root / album + items = _scan_folder(folder) + existing_filenames = set() + + for item in items: + existing_filenames.add(item["filename"]) + result = db.upsert_photo(album, item["filename"], item["mtime"]) + if result == "added": + added += 1 + + removed += db.remove_missing_photos(album, existing_filenames) + + # 3. 썸네일 미생성 분 일괄 생성 + no_thumb = db.get_photos_without_thumb() + thumbs_generated = 0 + + for photo in no_thumb: + src = travel_root / photo["album"] / photo["filename"] + dest = thumb_root / photo["album"] / photo["filename"] + if _generate_thumb(src, dest): + db.mark_thumb_done(photo["album"], photo["filename"]) + thumbs_generated += 1 + + duration = round(time.time() - start, 2) + logger.info( + "Sync complete: added=%d removed=%d thumbs=%d duration=%.2fs", + added, removed, thumbs_generated, duration, + ) + + return { + "added": added, + "removed": removed, + "thumbs_generated": thumbs_generated, + "duration_sec": duration, + }