diff --git a/travel-proxy/app/indexer.py b/travel-proxy/app/indexer.py index fa6f85e..bba22a2 100644 --- a/travel-proxy/app/indexer.py +++ b/travel-proxy/app/indexer.py @@ -64,6 +64,64 @@ def _generate_thumb(src: Path, dest: Path) -> bool: return False +def _load_region_map_merged(region_map_path: Path, thumb_root: Path) -> dict: + """ + 원본 region_map.json + RW 오버라이드 파일 병합. + 원본은 RO 마운트이므로 신규 폴더는 thumb_root/region_map_extra.json에 저장. + """ + try: + with open(region_map_path, "r", encoding="utf-8") as f: + region_map = json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + logger.error("Failed to load region_map: %s", e) + raise + + # 오버라이드 파일 병합 + extra_path = thumb_root / "region_map_extra.json" + if extra_path.exists(): + try: + with open(extra_path, "r", encoding="utf-8") as f: + extra = json.load(f) + for k, v in extra.items(): + if k in region_map: + # 기존 지역에 앨범 추가 (중복 방지) + existing = region_map[k] + if isinstance(existing, list) and isinstance(v, list): + merged = list(dict.fromkeys(existing + v)) + region_map[k] = merged + else: + region_map[k] = v + except (json.JSONDecodeError, OSError) as e: + logger.warning("Failed to load region_map_extra: %s", e) + + return region_map + + +def _save_extra_albums(thumb_root: Path, new_folders: List[str]) -> None: + """신규 발견 폴더를 RW 영역의 오버라이드 파일에 저장.""" + extra_path = thumb_root / "region_map_extra.json" + extra = {} + if extra_path.exists(): + try: + with open(extra_path, "r", encoding="utf-8") as f: + extra = json.load(f) + except (json.JSONDecodeError, OSError): + extra = {} + + uncategorized = extra.get("미분류", []) + existing = set(uncategorized) + added = [f for f in new_folders if f not in existing] + if added: + uncategorized.extend(added) + extra["미분류"] = uncategorized + try: + with open(extra_path, "w", encoding="utf-8") as f: + json.dump(extra, f, ensure_ascii=False, indent=2) + logger.info("Saved %d new folders to region_map_extra.json → 미분류", len(added)) + except OSError as e: + logger.warning("Cannot write region_map_extra: %s", e) + + def sync( travel_root: Path, thumb_root: Path, @@ -73,17 +131,13 @@ def sync( 폴더 스캔 → DB 동기화 + 썸네일 일괄 생성. Returns: - {"added": int, "removed": int, "thumbs_generated": int, "duration_sec": float} + {"added": int, "removed": int, "thumbs_generated": int, + "discovered": int, "duration_sec": float} """ start = time.time() - # 1. region_map.json에서 전체 앨범 폴더 수집 - try: - with open(region_map_path, "r", encoding="utf-8") as f: - region_map = json.load(f) - except (FileNotFoundError, json.JSONDecodeError) as e: - logger.error("Failed to load region_map: %s", e) - raise + # 1. region_map.json + 오버라이드 병합 + region_map = _load_region_map_merged(region_map_path, thumb_root) all_albums: Set[str] = set() for v in region_map.values(): @@ -92,6 +146,35 @@ def sync( elif isinstance(v, dict) and isinstance(v.get("albums"), list): all_albums.update(v["albums"]) + # 1-b. 파일시스템에서 신규 폴더 자동 탐색 + discovered = 0 + new_folders = [] + try: + with os.scandir(travel_root) as entries: + for entry in entries: + if not entry.is_dir() or entry.name.startswith("_"): + continue + if entry.name in all_albums: + continue + # 이미지가 하나라도 있는 폴더만 추가 + folder = travel_root / entry.name + has_images = any( + Path(f.name).suffix.lower() in IMAGE_EXT + for f in os.scandir(folder) + if f.is_file() + ) + if has_images: + all_albums.add(entry.name) + new_folders.append(entry.name) + discovered += 1 + logger.info("Discovered new album folder: %s", entry.name) + except OSError as e: + logger.warning("Failed to scan travel_root for new folders: %s", e) + + # 신규 폴더를 RW 오버라이드 파일에 영구 저장 + if new_folders: + _save_extra_albums(thumb_root, new_folders) + # 2. 각 앨범 폴더 스캔 → DB 배치 동기화 added = 0 updated = 0 @@ -123,8 +206,8 @@ def sync( duration = round(time.time() - start, 2) logger.info( - "Sync complete: added=%d updated=%d removed=%d thumbs=%d duration=%.2fs", - added, updated, removed, thumbs_generated, duration, + "Sync complete: added=%d updated=%d removed=%d thumbs=%d discovered=%d duration=%.2fs", + added, updated, removed, thumbs_generated, discovered, duration, ) return { @@ -132,5 +215,6 @@ def sync( "updated": updated, "removed": removed, "thumbs_generated": thumbs_generated, + "discovered": discovered, "duration_sec": duration, } diff --git a/travel-proxy/app/main.py b/travel-proxy/app/main.py index 41788dc..df26347 100644 --- a/travel-proxy/app/main.py +++ b/travel-proxy/app/main.py @@ -10,7 +10,7 @@ from pydantic import BaseModel from PIL import Image from .db import init_db, get_photos_by_region, get_all_albums, set_album_cover, mark_thumb_done -from .indexer import sync +from .indexer import sync, _load_region_map_merged logger = logging.getLogger(__name__) @@ -49,7 +49,7 @@ def _read_json(path: Path) -> Any: def load_region_map() -> dict: - return _read_json(REGION_MAP_PATH) + return _load_region_map_merged(REGION_MAP_PATH, THUMB_ROOT) def load_regions_geojson() -> dict: @@ -115,7 +115,20 @@ def health(): @app.get("/api/travel/regions") def regions(): - return load_regions_geojson() + geojson = load_regions_geojson() + # 미분류 지역이 region_map에 있으면 GeoJSON에도 동적 추가 + region_map = load_region_map() + existing_ids = { + f.get("properties", {}).get("id") + for f in geojson.get("features", []) + } + if "미분류" in region_map and "미분류" not in existing_ids: + geojson.setdefault("features", []).append({ + "type": "Feature", + "properties": {"id": "미분류", "name": "미분류"}, + "geometry": None, + }) + return geojson @app.get("/api/travel/photos")