# ────────────────────────────────────────────────────────── import os, 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}"} # 2. BEST 탭 데이터 BEST_FILE = "best_games.json" PER_PAGE = 48 def _init_best(): if not os.path.exists(BEST_FILE): json.dump([], open(BEST_FILE, "w")) def _load_best(): try: return json.load(open(BEST_FILE)) except Exception: return [] # 3. 모든 배포 가져오기 (v6) def fetch_all(limit=200): try: params = {"limit": limit} if TEAM: params["teamId"] = TEAM r = requests.get(f"{API}/v6/deployments", headers=HEAD, params=params, timeout=30) r.raise_for_status() out = [] for d in r.json().get("deployments", []): if d.get("state") != "READY": continue created_ms = d.get("created", time.time()*1000) url_host = d.get("url", "") url_full = url_host if url_host.startswith("http") else f"https://{url_host}" out.append({ "title": d.get("name", "(제목 없음)"), "url" : url_full, "ts" : int(created_ms / 1000) }) return sorted(out, key=lambda x: x["ts"], reverse=True) except Exception as e: print("Vercel API 오류:", e) 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 "
표시할 배포가 없습니다.
" css = r""" """ h = css + "
" for c in cards: date = datetime.datetime.fromtimestamp(c["ts"]).strftime("%Y-%m-%d") h += f"""

{c['title']}

{date}

""" h += "

Page "+str(pg)+" / "+str(total)+"

" return h # 6. Gradio UI def build(): _init_best() with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo: gr.Markdown("

🎮 Vibe Game Craft

") with gr.Row(): b_new = gr.Button("NEW", size="sm") b_best = gr.Button("BEST", size="sm") b_prev = gr.Button("⬅️ Prev", size="sm") b_next = gr.Button("Next ➡️", size="sm") b_ref = gr.Button("🔄 Reload", size="sm") tab = gr.State("new") # "new" / "best" np = gr.State(1) # NEW 페이지 bp = gr.State(1) # BEST 페이지 out = gr.HTML() # NEW / BEST 렌더 def show_new(p=1): data, tot = page(fetch_all(), p) return html(data, p, tot), "new", p def show_best(p=1): data, tot = page(_load_best(), p) return html(data, p, tot), "best", p # Prev / Next def prev(t, n, b): if t=="new": n=max(1,n-1); h,_,_ = show_new(n); return h, n, b b=max(1,b-1); h,_,_ = show_best(b); return h, n, b def nxt(t, n, b): if t=="new": maxp=(len(fetch_all())+PER_PAGE-1)//PER_PAGE n=min(maxp,n+1); h,_,_ = show_new(n); return h, n, b maxp=(len(_load_best())+PER_PAGE-1)//PER_PAGE b=min(maxp,b+1); h,_,_ = show_best(b); return h, n, b # Reload def reload(t, n, b): return show_new(n)[0] if t=="new" else show_best(b)[0] # 버튼 연결 b_new.click(show_new, outputs=[out, tab, np]) b_best.click(show_best, outputs=[out, tab, bp]) b_prev.click(prev, inputs=[tab, np, bp], outputs=[out, np, bp]) b_next.click(nxt, inputs=[tab, np, bp], outputs=[out, np, bp]) b_ref.click(reload, inputs=[tab, np, bp], outputs=[out]) # 초기 로드 — NEW 1페이지 demo.load(show_new, outputs=[out, tab, np]) return demo # 7. 실행 app = build() if __name__ == "__main__": app.launch()