Spaces:
Sleeping
Sleeping
| 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 "<div style='text-align:center;padding:70px;color:#555;'>ํ์ํ ๋ฐฐํฌ๊ฐ ์์ต๋๋ค.</div>" | |
| css=r""" | |
| <style> | |
| body{margin:0;font-family:Poppins,sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);} | |
| .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px 24px;margin:0 20px 60px;} | |
| @media(max-width:1024px){.grid{grid-template-columns:repeat(2,1fr);} } | |
| @media(max-width:640px){ .grid{grid-template-columns:1fr;} } | |
| .card{background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 10px 25px rgba(0,0,0,.08);transition:.3s} | |
| .card:hover{transform:translateY(-6px);box-shadow:0 16px 40px rgba(0,0,0,.12)} | |
| .hdr{padding:20px 24px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee;} | |
| .ttl{margin:0;font-size:1.15rem;font-weight:700;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;} | |
| .date{margin-top:4px;font-size:.85rem;color:#777;} | |
| .frame{position:relative;width:100%;padding-top:60%;overflow:hidden;} | |
| .frame iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%; | |
| transform:scale(.7);transform-origin:top left;border:0;} | |
| .foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;} | |
| .link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;} | |
| .cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;} | |
| </style>""" | |
| h=css+"<div class='grid'>" | |
| for c in cards: | |
| date=datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d") | |
| h+=f""" | |
| <div class='card'> | |
| <div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div> | |
| <div class='frame'><iframe src="{c['url']}" loading="lazy" | |
| allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div> | |
| <div class='foot'><a class='link' href="{c['url']}" target="_blank">์๋ณธโ</a></div> | |
| </div>""" | |
| h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>" | |
| 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("<h1 style='text-align:center;padding:32px 0 0;color:#333;'>๐ฎ Vibe Game Craft</h1>") | |
| 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"); np = gr.State(1); bp = gr.State(1); out = gr.HTML() | |
| def show_new(p=1): d,t=page(fetch_all(),p); return html(d,p,t),"new",p | |
| def show_best(p=1): d,t=page(_load_best(),p); return html(d,p,t),"best",p | |
| 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 | |
| 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(lambda t,n,b: show_new(n)[0] if t=="new" else show_best(b)[0], | |
| inputs=[tab,np,bp], outputs=[out]) | |
| demo.load(show_new, outputs=[out,tab,np]) | |
| return demo | |
| app = build() | |
| if __name__ == "__main__": | |
| app.launch() | |