diff --git a/travel-proxy/app/main.py b/travel-proxy/app/main.py index 20c0d35..8df391a 100644 --- a/travel-proxy/app/main.py +++ b/travel-proxy/app/main.py @@ -149,17 +149,20 @@ def scan_album(album: str) -> List[Dict[str, Any]]: return [] items = [] - for p in album_dir.iterdir(): - if p.is_file() and p.suffix.lower() in IMAGE_EXT: - # ✅ 썸네일 생성 보장 - ensure_thumb(p, album) - - items.append({ - "album": album, - "file": p.name, - "url": f"{MEDIA_BASE}/{album}/{p.name}", - "thumb": f"{MEDIA_BASE}/.thumb/{album}/{p.name}", - }) + # os.scandir이 iterdir보다 빠름 (파일 속성 한 번에 가져옴) + with os.scandir(album_dir) as entries: + for entry in entries: + if entry.is_file() and Path(entry.name).suffix.lower() in IMAGE_EXT: + # ⚡ 성능 핵심: 여기서 썸네일 생성(ensure_thumb) 절대 하지 않음! + # 파일 존재 여부만 확인하고 바로 리턴 + items.append({ + "album": album, + "file": entry.name, + "url": f"{MEDIA_BASE}/{album}/{entry.name}", + "thumb": f"{MEDIA_BASE}/.thumb/{album}/{entry.name}", + # 정렬을 위해 수정시간/이름 필요하면 여기서 저장 + "mtime": entry.stat().st_mtime + }) return items # ----------------------------- @@ -174,40 +177,58 @@ def regions(): @app.get("/api/travel/photos") def photos( region: str = Query(...), - limit: int = Query(500, le=5000), + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), ): _meta_changed_invalidate_cache() + # 1. 캐시 키에 페이지 정보 포함하지 않음 (전체 리스트 캐싱 후 슬라이싱 전략) + # 왜냐하면 파일 스캔 비용이 크지, 메모리 슬라이싱 비용은 작기 때문. now = time.time() cached = CACHE.get(region) - if cached and now - cached["ts"] < CACHE_TTL: - return cached["data"] + + if not cached or now - cached["ts"] >= CACHE_TTL: + region_map = load_region_map() + albums = _get_albums_for_region(region, region_map) - region_map = load_region_map() - albums = _get_albums_for_region(region, region_map) + all_items = [] + matched = [] - all_items = [] - matched = [] + for album in albums: + items = scan_album(album) + matched.append({"album": album, "count": len(items)}) + all_items.extend(items) - for album in albums: - items = scan_album(album) - matched.append({"album": album, "count": len(items)}) - all_items.extend(items) + # 정렬: 앨범명 > 파일명 (또는 찍은 날짜) + all_items.sort(key=lambda x: (x["album"], x["file"])) + + cached_data = { + "region": region, + "matched_albums": matched, + "items": all_items, + "total": len(all_items), + } + CACHE[region] = {"ts": now, "data": cached_data} + else: + cached_data = cached["data"] - all_items.sort(key=lambda x: (x["album"], x["file"])) + # 2. 페이지네이션 슬라이싱 + all_items = cached_data["items"] + total = len(all_items) + start = (page - 1) * size + end = start + size + + paged_items = all_items[start:end] - data = { + return { "region": region, - "matched_albums": matched, - "items": all_items[:limit], - "total": len(all_items), - "cached_at": int(now), - "cache_ttl": CACHE_TTL, + "page": page, + "size": size, + "total": total, + "has_next": end < total, + "items": paged_items, } - CACHE[region] = {"ts": now, "data": data} - return data - @app.get("/media/travel/.thumb/{album}/{filename}") def get_thumb(album: str, filename: str):