Spaces:
Running
Running
import os, time, json, requests, datetime, 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(): | |
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")) | |
def load_best_games(): | |
try: | |
return json.load(open(BEST_GAMES_FILE)) | |
except Exception: | |
return [] | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 4. ์ต์ Vercel ๋ฐฐํฌ โ ๊ฒ์ ๋ชฉ๋ก | |
def get_latest_deployments(): | |
try: | |
r = requests.get( | |
f"{VERCEL_API_URL}/deployments", | |
headers=HEADERS, | |
params={"limit": 100}, | |
timeout=30, | |
) | |
r.raise_for_status() | |
games = [] | |
for d in r.json().get("deployments", []): | |
if d.get("state") != "READY": | |
continue | |
ts = ( | |
int(d["createdAt"] / 1000) | |
if isinstance(d["createdAt"], (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, | |
} | |
) | |
return sorted(games, key=lambda x: x["timestamp"], reverse=True) | |
except Exception as e: | |
print("Vercel API ์ค๋ฅ:", e) | |
return [] | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 5. ํ์ด์ง๋ค์ด์ ํฌํผ | |
def paginate(lst, page): | |
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 (์ฌ์ดํธ ๋ฏธ๋ฌ๋ง ์คํ์ผ) | |
def generate_gallery_html(games, page, total_pages, tab_name): | |
if not games: | |
return ( | |
"<div style='text-align:center;padding:60px;'>ํ์ํ ๊ฒ์์ด ์์ต๋๋ค.</div>" | |
) | |
#โCSS (ํ์ ์ต์๋ง ์ธ๋ผ์ธ - reference ์คํ์ผ ์ฌ๊ตฌ์ฑ) | |
css = """ | |
<style> | |
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:24px;margin-bottom:40px} | |
.item{background:white;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{font-size:1.1rem;font-weight:700;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} | |
.meta{font-size:.85rem;color:#666;margin-top:4px;display:flex;justify-content:space-between;align-items:center} | |
.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:white;padding:6px 14px;border-radius:8px;cursor:pointer} | |
.pg button.active,.pg button:hover{background:#9c89b8;color:white;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'><span>{date}</span></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>" | |
if total_pages > 1: | |
html += "<div class='pg'>" | |
if page > 1: | |
html += ( | |
f"<button onclick=\"refreshGallery('{tab_name}',{page-1})\">์ด์ </button>" | |
) | |
for p in range(max(1, page - 2), min(total_pages, page + 2) + 1): | |
active = "active" if p == page else "" | |
html += ( | |
f"<button class='{active}' onclick=\"refreshGallery('{tab_name}',{p})\">{p}</button>" | |
) | |
if page < total_pages: | |
html += ( | |
f"<button onclick=\"refreshGallery('{tab_name}',{page+1})\">๋ค์</button>" | |
) | |
html += "</div>" | |
#โrefreshGallery JS (ํญ ๋ฒํผ ์ฌ์ฌ์ฉ) | |
html += """ | |
<script> | |
function refreshGallery(t,p){document.getElementById(t+'-btn').click(); | |
window.setTimeout(()=>window.__setPage&&__setPage(t,p),50);} | |
</script> | |
""" | |
return html | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 7. Gradio UI | |
def create_gallery_interface(): | |
initialize_best_games() | |
with gr.Blocks() 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("๐ ์๋ก๊ณ ์นจ") | |
tab_state = gr.State("new") | |
page_state = gr.State(1) # NEW ํญ ํ์ฌ ํ์ด์ง | |
page_state2 = gr.State(1) # BEST ํญ ํ์ฌ ํ์ด์ง | |
gallery_out = gr.HTML() | |
#โ๊ฐ ํญ ๋ ๋ ํจ์ | |
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_out, tab_state, page_state]) | |
best_btn.click(fn=render_best, outputs=[gallery_out, tab_state, page_state2]) | |
#โ์๋ก๊ณ ์นจ | |
def do_refresh(tab, p1, p2): | |
if tab == "new": | |
return render_new(p1)[0] | |
return render_best(p2)[0] | |
refresh.click(fn=do_refresh, | |
inputs=[tab_state, page_state, page_state2], | |
outputs=[gallery_out]) | |
#โํ์ด์ง๋ค์ด์ ์ ์ํ JS ์ฝ๋ฐฑ์ฉ ์จ์ ํจ์ | |
def set_page(tab, p): | |
if tab == "new": | |
return render_new(p) | |
return render_best(p) | |
#โ์ด๊ธฐ NEW | |
demo.load(fn=render_new, outputs=[gallery_out, tab_state, page_state]) | |
#โJS ์์ ํธ์ถ๋ ์ ์๋๋ก ํจ์ ๋ ธ์ถ | |
demo.set_event_trigger("__setPage", set_page, [tab_state, page_state, page_state2], [gallery_out, tab_state, page_state, page_state2]) | |
return demo | |
# โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
# 8. ์คํ (Gradio Spaces ์๋ ๊ฐ์ง๋ฅผ ์ํด ์ ์ญ app ๋ณ์ ๋ ธ์ถ) | |
app = create_gallery_interface() | |
if __name__ == "__main__": | |
app.launch() | |