import os, re, time, json, datetime, requests, gradio as gr # ───────────────────── 1. Vercel API ───────────────────── TOKEN = os.getenv("SVR_TOKEN") TEAM = os.getenv("VERCEL_TEAM_ID") if not TOKEN: raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.") API = "https://api.vercel.com" HEAD = {"Authorization": f"Bearer {TOKEN}"} print(f"API 토큰: {TOKEN[:4]}... (마스킹됨)") print(f"팀 ID: {TEAM if TEAM else '없음'}") # ───────────────────── 2. BEST 데이터 ──────────────────── BEST_FILE, PER_PAGE = "best_games.json", 48 def _init_best(): if not os.path.exists(BEST_FILE): json.dump([], open(BEST_FILE, "w")) def _load_best(): try: data = json.load(open(BEST_FILE)) for it in data: if "ts" not in it: it["ts"] = int(it.get("timestamp", time.time())) return data except Exception as e: print(f"BEST 데이터 로드 오류: {e}") return [] def fetch_all(limit=50): """ Vercel API v10/projects를 사용하여 프로젝트 목록을 가져옵니다. """ try: # API 파라미터 설정 params = {"limit": limit} if TEAM: params["teamId"] = TEAM print(f"Vercel API 호출 (프로젝트): {API}/v10/projects (params={params})") resp = requests.get(f"{API}/v10/projects", headers=HEAD, params=params, timeout=30) print(f"API 응답 상태 코드: {resp.status_code}") if resp.status_code != 200: print(f"API 응답 오류: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}") return [] data = resp.json() if "projects" not in data: print(f"API 응답에 projects 필드가 없습니다: {str(data)[:200]}...") return [] projects = data.get("projects", []) print(f"{len(projects)}개의 프로젝트를 찾았습니다") # 각 프로젝트의 최신 배포 정보 가져오기 games = [] for project in projects: project_id = project.get("id") project_name = project.get("name", "(제목 없음)") # 프로젝트별 최신 배포 가져오기 (projectId 필터 추가) try: deploy_params = { "limit": 1, "projectId": project_id, # 중요: 해당 프로젝트의 배포만 필터링 "state": "READY" } if TEAM: deploy_params["teamId"] = TEAM print(f"프로젝트 {project_id} ({project_name}) 배포 조회 중...") deploy_resp = requests.get( f"{API}/v6/deployments", headers=HEAD, params=deploy_params, timeout=30 ) if deploy_resp.status_code == 200: deploy_data = deploy_resp.json() deployments = deploy_data.get("deployments", []) if deployments: deployment = deployments[0] url = deployment.get("url", "") if url: games.append({ "title": project_name, "url": f"https://{url}", "ts": int(deployment.get("created", time.time() * 1000) / 1000), "projectId": project_id }) print(f"프로젝트 {project_name}의 배포 URL: https://{url}") else: print(f"프로젝트 {project_name}에 배포된 버전이 없습니다.") except Exception as e: print(f"프로젝트 {project_id}의 배포 정보 가져오기 실패: {e}") continue print(f"총 {len(games)}개의 유효한 배포를 찾았습니다") return sorted(games, key=lambda x: x["ts"], reverse=True) except Exception as e: print(f"Vercel API 오류: {str(e)}") import traceback traceback.print_exc() return [] # ───────────────────── 4. 페이지네이션 ─────────────────── def page(lst, pg): s=(pg-1)*PER_PAGE; e=s+PER_PAGE total=(len(lst)+PER_PAGE-1)//PER_PAGE return lst[s:e], total # ───────────────────── 5. HTML 3-열 그리드 ─────────────── def html(cards, pg, total): if not cards: return "
{c['title']}
{date}
Page "+str(pg)+" / "+str(total)+"
" return h # ───────────────────── 6. Gradio Blocks UI ─────────────── def build(): _init_best() with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo: gr.Markdown("