Spaces:
Running
Running
""" | |
Vibe Game Craft โ NEW / BEST ํญ์ โ์ฌ์ดํธ ๋ฏธ๋ฌ๋งโ(iframe) ๋ฐฉ์์ผ๋ก ๋ณด์ฌ์ฃผ๋ Gradio Space | |
โ Gradio 4.x ๊ณต์ฉ API๋ง ์ฌ์ฉ โ set_event_trigger ์ ๊ฑฐ | |
โ Prev / Next ๋ฒํผ์ผ๋ก ํ์ด์ง ์ ํ | |
โ os.getenv("SVR_TOKEN") ํ์ | |
""" | |
import os | |
import time | |
import json | |
import requests | |
import datetime | |
import gradio as gr | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 1. Vercel API ์ค์ | |
VERCEL_API_TOKEN = os.getenv("SVR_TOKEN") # โ ํ๊ฒฝ๋ณ์์์ ์ฝ์ | |
if not VERCEL_API_TOKEN: | |
raise EnvironmentError("ํ๊ฒฝ ๋ณ์ 'SVR_TOKEN'์ด ์ค์ ๋์ด ์์ง ์์ต๋๋ค!") | |
VERCEL_API_URL = "https://api.vercel.com/v9" | |
HEADERS = { | |
"Authorization": f"Bearer {VERCEL_API_TOKEN}", | |
"Content-Type": "application/json", | |
} | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 2. ๊ฐค๋ฌ๋ฆฌ ์ค์ | |
BEST_GAMES_FILE = "best_games.json" | |
GAMES_PER_PAGE = 48 | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 3. BEST ํญ ์ด๊ธฐํ / ๋ก๋ | |
def initialize_best_games() -> None: | |
if not os.path.exists(BEST_GAMES_FILE): | |
sample = [ | |
{ | |
"title": "ํ ํธ๋ฆฌ์ค", | |
"description": "ํด๋์ ํ ํธ๋ฆฌ์ค ๊ฒ์", | |
"url": "https://tmkdop.vercel.app", | |
"timestamp": time.time(), | |
}, | |
{ | |
"title": "์ค๋ค์ดํฌ", | |
"description": "์ ํต์ ์ธ ์ค๋ค์ดํฌ ๊ฒ์", | |
"url": "https://tmkdop.vercel.app", | |
"timestamp": time.time(), | |
}, | |
{ | |
"title": "ํฉ๋งจ", | |
"description": "๊ณ ์ ์์ผ์ด๋ ๊ฒ์", | |
"url": "https://tmkdop.vercel.app", | |
"timestamp": time.time(), | |
}, | |
] | |
json.dump(sample, open(BEST_GAMES_FILE, "w"), ensure_ascii=False, indent=2) | |
def load_best_games() -> list: | |
try: | |
return json.load(open(BEST_GAMES_FILE)) | |
except Exception: | |
return [] | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 4. Vercel ์ต์ ๋ฐฐํฌ ๊ฐ์ ธ์ค๊ธฐ | |
def get_latest_deployments() -> list: | |
try: | |
resp = requests.get( | |
f"{VERCEL_API_URL}/deployments", | |
headers=HEADERS, | |
params={"limit": 100}, | |
timeout=30, | |
) | |
resp.raise_for_status() | |
games = [] | |
for d in resp.json().get("deployments", []): | |
if d.get("state") != "READY": | |
continue | |
created_at = d.get("createdAt", time.time() * 1000) | |
ts = int(created_at / 1000) if isinstance(created_at, (int, float)) else int( | |
time.time() | |
) | |
games.append( | |
{ | |
"title": d.get("name", "๊ฒ์"), | |
"description": f"๋ฐฐํฌ๋ ๊ฒ์: {d.get('name')}", | |
"url": f"https://{d.get('url')}", | |
"timestamp": ts, | |
} | |
) | |
games.sort(key=lambda x: x["timestamp"], reverse=True) | |
return games | |
except Exception as e: | |
print("Vercel API ์ค๋ฅ:", e) | |
return [] | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 5. ํ์ด์ง๋ค์ด์ | |
def paginate(lst: list, page: int): | |
start = (page - 1) * GAMES_PER_PAGE | |
end = start + GAMES_PER_PAGE | |
total = (len(lst) + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE | |
return lst[start:end], total | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 6. ๊ฐค๋ฌ๋ฆฌ HTML (iframe ๋ฏธ๋ฌ๋ง) | |
def generate_gallery_html(games: list, page: int, total_pages: int, tab_name: str) -> str: | |
if not games: | |
return "<div style='text-align:center;padding:60px;'>ํ์ํ ๊ฒ์์ด ์์ต๋๋ค.</div>" | |
css = """ | |
<style> | |
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:24px;margin-bottom:40px} | |
.item{background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,.1);transition:.3s} | |
.item:hover{transform:translateY(-6px);box-shadow:0 12px 30px rgba(0,0,0,.15)} | |
.hdr{padding:14px 18px;background:rgba(255,255,255,.8);backdrop-filter:blur(6px);border-bottom:1px solid #eee} | |
.ttl{margin:0;font-size:1.1rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
.meta{font-size:.85rem;color:#666;margin-top:4px} | |
.iframe-box{position:relative;width:100%;padding-top:60%;overflow:hidden} | |
.iframe-box iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%; | |
transform:scale(.7);transform-origin:top left;border:0} | |
.footer{padding:10px 16px;background:rgba(255,255,255,.85);backdrop-filter:blur(6px);text-align:right} | |
.footer a{font-size:.85rem;font-weight:600;color:#2c5282;text-decoration:none} | |
.pg{display:flex;justify-content:center;gap:10px;margin-top:10px} | |
.pg button{border:1px solid #ddd;background:#fff;padding:6px 14px;border-radius:8px;cursor:pointer} | |
.pg button.active,.pg button:hover{background:#9c89b8;color:#fff;border-color:#9c89b8} | |
</style> | |
""" | |
html = css + "<div class='grid'>" | |
for g in games: | |
date = datetime.datetime.fromtimestamp(g["timestamp"]).strftime("%Y-%m-%d") | |
html += f""" | |
<div class='item'> | |
<div class='hdr'> | |
<p class='ttl'>{g['title']}</p> | |
<div class='meta'>{date}</div> | |
</div> | |
<div class='iframe-box'><iframe src="{g['url']}" loading="lazy" | |
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div> | |
<div class='footer'><a href="{g['url']}" target="_blank">์๋ณธ ์ฌ์ดํธ ์ด๊ธฐ โ</a></div> | |
</div> | |
""" | |
html += "</div>" | |
# ํ์ด์ง ๋ฒํธ(์ฝ๊ธฐ ์ ์ฉ) โ Prev/Next ๋ Gradio์์ ์ฒ๋ฆฌ | |
html += f"<div style='text-align:center;margin-top:4px;font-size:.85rem;color:#666;'>Page {page} / {total_pages}</div>" | |
return html | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 7. Gradio UI | |
def create_gallery_interface(): | |
initialize_best_games() | |
with gr.Blocks(title="Vibe Game Craft") as demo: | |
gr.HTML("<h1 style='text-align:center;'>๐ฎ Vibe Game Craft</h1>") | |
with gr.Row(): | |
new_btn = gr.Button("NEW", elem_id="new-btn") | |
best_btn = gr.Button("BEST", elem_id="best-btn") | |
refresh = gr.Button("๐ ์๋ก๊ณ ์นจ") | |
prev_btn = gr.Button("โฌ ๏ธ Prev") | |
next_btn = gr.Button("Next โก๏ธ") | |
# ์ํ | |
current_tab = gr.State("new") # "new" or "best" | |
new_page = gr.State(1) | |
best_page = gr.State(1) | |
gallery_html = gr.HTML() | |
# NEW / BEST ๋ ๋ ํจ์ | |
def render_new(page=1): | |
games, total = paginate(get_latest_deployments(), page) | |
return generate_gallery_html(games, page, total, "new"), "new", page | |
def render_best(page=1): | |
games, total = paginate(load_best_games(), page) | |
return generate_gallery_html(games, page, total, "best"), "best", page | |
# ๋ฒํผ ์ด๋ฒคํธ | |
new_btn.click(fn=render_new, | |
outputs=[gallery_html, current_tab, new_page]) | |
best_btn.click(fn=render_best, | |
outputs=[gallery_html, current_tab, best_page]) | |
def refresh_gallery(tab, p_new, p_best): | |
if tab == "new": | |
return render_new(p_new)[0] | |
return render_best(p_best)[0] | |
refresh.click(fn=refresh_gallery, | |
inputs=[current_tab, new_page, best_page], | |
outputs=[gallery_html]) | |
# Prev / Next | |
def prev_page(tab, p_new, p_best): | |
if tab == "new": | |
p_new = max(1, p_new - 1) | |
html, _, _ = render_new(p_new) | |
return html, p_new, p_best | |
p_best = max(1, p_best - 1) | |
html, _, _ = render_best(p_best) | |
return html, p_new, p_best | |
def next_page(tab, p_new, p_best): | |
if tab == "new": | |
games_total = len(get_latest_deployments()) | |
max_pages = (games_total + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE | |
p_new = min(max_pages, p_new + 1) | |
html, _, _ = render_new(p_new) | |
return html, p_new, p_best | |
games_total = len(load_best_games()) | |
max_pages = (games_total + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE | |
p_best = min(max_pages, p_best + 1) | |
html, _, _ = render_best(p_best) | |
return html, p_new, p_best | |
prev_btn.click(fn=prev_page, | |
inputs=[current_tab, new_page, best_page], | |
outputs=[gallery_html, new_page, best_page]) | |
next_btn.click(fn=next_page, | |
inputs=[current_tab, new_page, best_page], | |
outputs=[gallery_html, new_page, best_page]) | |
# ์ด๊ธฐ NEW ํญ ํ์ | |
demo.load(fn=render_new, | |
outputs=[gallery_html, current_tab, new_page]) | |
return demo | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 8. ์ ์ญ app (Spaces ์๋ ๊ฐ์ง) | |
app = create_gallery_interface() | |
if __name__ == "__main__": | |
app.launch() | |