Spaces:
Running
Running
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
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 "<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(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 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") # "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() | |