"""썸네일이 없는 발행 사이트에 썸네일을 채운다.""" import argparse, asyncio, os, sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) os.environ.setdefault("APP_ENV", "local") from sqlalchemy import select # noqa: E402 from common.database.db_session_manager import DB_SESSION_MNG # noqa: E402 from common.database.model.models import places, site_versions, sites # noqa: E402 from common.enums import DBWRType, ErrorType, SiteStatus # noqa: E402 from crud.site_crud import SiteCRUD # noqa: E402 from services import site_payload, site_thumbnail # noqa: E402 _crud = SiteCRUD() def _targets(only_missing: bool): """발행된 사이트 + 그 사이트의 현재 버전 스냅샷.""" stmt = ( select(sites, places, site_versions) .join(places, places.place_id == sites.place_id) .join(site_versions, site_versions.site_version_id == sites.current_version_id) .where( sites.deleted == False, # noqa: E712 sites.status == SiteStatus.PUBLISHED.value, ) ) if only_missing: stmt = stmt.where(sites.thumbnail_url.is_(None)) async def run_query(session): return await DB_SESSION_MNG.execute(session, stmt) return run_query async def run(dry_run: bool, only_missing: bool) -> None: # -dry-run 은 목록만 본다 — Azure 설정 없이도 대상이 맞는지 확인할 수 있어야 한다. if not dry_run and not site_thumbnail.is_configured(): raise SystemExit( "AZURE_STORAGE_CONNECTION_STRING 이 없습니다 — 업로드할 곳이 없어 아무것도 하지 않습니다." ) err, rows = await DB_SESSION_MNG.execute_lambda( sites.DBType(), DBWRType.DB_READ.value, _targets(only_missing) ) if err != ErrorType.SUCCESS: raise SystemExit(f"대상 조회 실패: {err}") print(f"[thumb] 대상 {len(rows)}건 ({'없는 것만' if only_missing else '전부'})") made = skipped = failed = 0 for site, place, version in rows: slug = site_payload.publish_slug(place, site) snapshot = version.snapshot or {} if dry_run: has_photo = bool(site_payload.primary_media(snapshot)) print(f" - {slug:<24} {place.name} {'' if has_photo else '(대표 사진 없음 — 건너뜀)'}") continue try: url = await site_thumbnail.store(slug, snapshot) except Exception as ex: # noqa: BLE001 — 한 건 실패가 나머지를 막지 않는다 print(f" ✗ {slug}: {type(ex).__name__}: {ex}") failed += 1 continue if not url: skipped += 1 continue await DB_SESSION_MNG.execute_lambda_claim( sites.DBType(), lambda s, sid=site.site_id, u=url: _crud.update_site(s, sid, {"thumbnail_url": u}) ) print(f" ✓ {slug} → {url}") made += 1 if not dry_run: print(f"[thumb] 완료 — 만듦 {made} · 사진 없음 {skipped} · 실패 {failed}") await DB_SESSION_MNG.dispose_all() def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--dry-run", action="store_true", help="업로드 없이 대상만 출력") parser.add_argument("--all", action="store_true", help="이미 썸네일이 있는 사이트도 다시 만든다") args = parser.parse_args() asyncio.run(run(args.dry_run, only_missing=not args.all)) if __name__ == "__main__": main()