Game-Gallery / app.py
ginipick's picture
Update app.py
2312f14 verified
raw
history blame
10.8 kB
"""
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()