Spaces:
Running
Running
Delete app-backup.py
Browse files- app-backup.py +0 -202
app-backup.py
DELETED
@@ -1,202 +0,0 @@
|
|
1 |
-
import os, re, time, json, datetime, requests, gradio as gr
|
2 |
-
|
3 |
-
# ───────────────────── 1. Vercel API ─────────────────────
|
4 |
-
TOKEN = os.getenv("SVR_TOKEN")
|
5 |
-
TEAM = os.getenv("VERCEL_TEAM_ID")
|
6 |
-
if not TOKEN:
|
7 |
-
raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.")
|
8 |
-
API = "https://api.vercel.com"
|
9 |
-
HEAD = {"Authorization": f"Bearer {TOKEN}"}
|
10 |
-
|
11 |
-
print(f"API 토큰: {TOKEN[:4]}... (마스킹됨)")
|
12 |
-
print(f"팀 ID: {TEAM if TEAM else '없음'}")
|
13 |
-
|
14 |
-
# ───────────────────── 2. BEST 데이터 ────────────────────
|
15 |
-
BEST_FILE, PER_PAGE = "best_games.json", 48
|
16 |
-
def _init_best():
|
17 |
-
if not os.path.exists(BEST_FILE):
|
18 |
-
json.dump([], open(BEST_FILE, "w"))
|
19 |
-
def _load_best():
|
20 |
-
try:
|
21 |
-
data = json.load(open(BEST_FILE))
|
22 |
-
for it in data:
|
23 |
-
if "ts" not in it:
|
24 |
-
it["ts"] = int(it.get("timestamp", time.time()))
|
25 |
-
return data
|
26 |
-
except Exception as e:
|
27 |
-
print(f"BEST 데이터 로드 오류: {e}")
|
28 |
-
return []
|
29 |
-
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
def fetch_all(limit=50):
|
35 |
-
"""
|
36 |
-
Vercel API v10/projects를 사용하여 프로젝트 목록을 가져옵니다.
|
37 |
-
"""
|
38 |
-
try:
|
39 |
-
# API 파라미터 설정
|
40 |
-
params = {"limit": limit}
|
41 |
-
if TEAM:
|
42 |
-
params["teamId"] = TEAM
|
43 |
-
|
44 |
-
print(f"Vercel API 호출 (프로젝트): {API}/v10/projects (params={params})")
|
45 |
-
|
46 |
-
resp = requests.get(f"{API}/v10/projects",
|
47 |
-
headers=HEAD, params=params, timeout=30)
|
48 |
-
|
49 |
-
print(f"API 응답 상태 코드: {resp.status_code}")
|
50 |
-
if resp.status_code != 200:
|
51 |
-
print(f"API 응답 오류: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
|
52 |
-
return []
|
53 |
-
|
54 |
-
data = resp.json()
|
55 |
-
|
56 |
-
if "projects" not in data:
|
57 |
-
print(f"API 응답에 projects 필드가 없습니다: {str(data)[:200]}...")
|
58 |
-
return []
|
59 |
-
|
60 |
-
projects = data.get("projects", [])
|
61 |
-
print(f"{len(projects)}개의 프로젝트를 찾았습니다")
|
62 |
-
|
63 |
-
# 각 프로젝트의 최신 배포 정보 가져오기
|
64 |
-
games = []
|
65 |
-
for project in projects:
|
66 |
-
project_id = project.get("id")
|
67 |
-
project_name = project.get("name", "(제목 없음)")
|
68 |
-
|
69 |
-
# 프로젝트별 최신 배포 가져오기 (projectId 필터 추가)
|
70 |
-
try:
|
71 |
-
deploy_params = {
|
72 |
-
"limit": 1,
|
73 |
-
"projectId": project_id, # 중요: 해당 프로젝트의 배포만 필터링
|
74 |
-
"state": "READY"
|
75 |
-
}
|
76 |
-
|
77 |
-
if TEAM:
|
78 |
-
deploy_params["teamId"] = TEAM
|
79 |
-
|
80 |
-
print(f"프로젝트 {project_id} ({project_name}) 배포 조회 중...")
|
81 |
-
|
82 |
-
deploy_resp = requests.get(
|
83 |
-
f"{API}/v6/deployments",
|
84 |
-
headers=HEAD,
|
85 |
-
params=deploy_params,
|
86 |
-
timeout=30
|
87 |
-
)
|
88 |
-
|
89 |
-
if deploy_resp.status_code == 200:
|
90 |
-
deploy_data = deploy_resp.json()
|
91 |
-
deployments = deploy_data.get("deployments", [])
|
92 |
-
|
93 |
-
if deployments:
|
94 |
-
deployment = deployments[0]
|
95 |
-
url = deployment.get("url", "")
|
96 |
-
if url:
|
97 |
-
games.append({
|
98 |
-
"title": project_name,
|
99 |
-
"url": f"https://{url}",
|
100 |
-
"ts": int(deployment.get("created", time.time() * 1000) / 1000),
|
101 |
-
"projectId": project_id
|
102 |
-
})
|
103 |
-
print(f"프로젝트 {project_name}의 배포 URL: https://{url}")
|
104 |
-
else:
|
105 |
-
print(f"프로젝트 {project_name}에 배포된 버전이 없습니다.")
|
106 |
-
except Exception as e:
|
107 |
-
print(f"프로젝트 {project_id}의 배포 정보 가져오기 실패: {e}")
|
108 |
-
continue
|
109 |
-
|
110 |
-
print(f"총 {len(games)}개의 유효한 배포를 찾았습니다")
|
111 |
-
return sorted(games, key=lambda x: x["ts"], reverse=True)
|
112 |
-
|
113 |
-
except Exception as e:
|
114 |
-
print(f"Vercel API 오류: {str(e)}")
|
115 |
-
import traceback
|
116 |
-
traceback.print_exc()
|
117 |
-
return []
|
118 |
-
|
119 |
-
# ───────────────────── 4. 페이지네이션 ───────────────────
|
120 |
-
def page(lst, pg):
|
121 |
-
s=(pg-1)*PER_PAGE; e=s+PER_PAGE
|
122 |
-
total=(len(lst)+PER_PAGE-1)//PER_PAGE
|
123 |
-
return lst[s:e], total
|
124 |
-
|
125 |
-
# ───────────────────── 5. HTML 3-열 그리드 ───────────────
|
126 |
-
def html(cards, pg, total):
|
127 |
-
if not cards:
|
128 |
-
return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
|
129 |
-
css=r"""
|
130 |
-
<style>
|
131 |
-
body{margin:0;font-family:Poppins,sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
|
132 |
-
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px 24px;margin:0 20px 60px;}
|
133 |
-
@media(max-width:1024px){.grid{grid-template-columns:repeat(2,1fr);} }
|
134 |
-
@media(max-width:640px){ .grid{grid-template-columns:1fr;} }
|
135 |
-
.card{background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 10px 25px rgba(0,0,0,.08);transition:.3s}
|
136 |
-
.card:hover{transform:translateY(-6px);box-shadow:0 16px 40px rgba(0,0,0,.12)}
|
137 |
-
.hdr{padding:20px 24px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee;}
|
138 |
-
.ttl{margin:0;font-size:1.15rem;font-weight:700;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
|
139 |
-
.date{margin-top:4px;font-size:.85rem;color:#777;}
|
140 |
-
.frame{position:relative;width:100%;padding-top:60%;overflow:hidden;}
|
141 |
-
.frame iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
|
142 |
-
transform:scale(.7);transform-origin:top left;border:0;}
|
143 |
-
.foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
|
144 |
-
.link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
|
145 |
-
.cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;}
|
146 |
-
</style>"""
|
147 |
-
|
148 |
-
h=css+"<div class='grid'>"
|
149 |
-
for c in cards:
|
150 |
-
date=datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d")
|
151 |
-
h+=f"""
|
152 |
-
<div class='card'>
|
153 |
-
<div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
|
154 |
-
<div class='frame'><iframe src="{c['url']}" loading="lazy"
|
155 |
-
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
|
156 |
-
<div class='foot'><a class='link' href="{c['url']}" target="_blank">원본↗</a></div>
|
157 |
-
</div>"""
|
158 |
-
h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
|
159 |
-
return h
|
160 |
-
|
161 |
-
# ───────────────────── 6. Gradio Blocks UI ───────────────
|
162 |
-
def build():
|
163 |
-
_init_best()
|
164 |
-
with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
|
165 |
-
gr.Markdown("<h1 style='text-align:center;padding:32px 0 0;color:#333;'>🎮 Vibe Game Craft</h1>")
|
166 |
-
with gr.Row():
|
167 |
-
b_new = gr.Button("NEW", size="sm")
|
168 |
-
b_best = gr.Button("BEST", size="sm")
|
169 |
-
b_prev = gr.Button("⬅️ Prev", size="sm")
|
170 |
-
b_next = gr.Button("Next ➡️", size="sm")
|
171 |
-
b_ref = gr.Button("🔄 Reload", size="sm")
|
172 |
-
|
173 |
-
tab = gr.State("new"); np = gr.State(1); bp = gr.State(1); out = gr.HTML()
|
174 |
-
|
175 |
-
def show_new(p=1): d,t=page(fetch_all(),p); return html(d,p,t),"new",p
|
176 |
-
def show_best(p=1): d,t=page(_load_best(),p); return html(d,p,t),"best",p
|
177 |
-
|
178 |
-
def prev(t,n,b):
|
179 |
-
if t=="new": n=max(1,n-1); h,_,_=show_new(n); return h,n,b
|
180 |
-
b=max(1,b-1); h,_,_=show_best(b); return h,n,b
|
181 |
-
def nxt(t,n,b):
|
182 |
-
if t=="new":
|
183 |
-
maxp=(len(fetch_all())+PER_PAGE-1)//PER_PAGE
|
184 |
-
n=min(maxp,n+1); h,_,_=show_new(n); return h,n,b
|
185 |
-
maxp=(len(_load_best())+PER_PAGE-1)//PER_PAGE
|
186 |
-
b=min(maxp,b+1); h,_,_=show_best(b); return h,n,b
|
187 |
-
|
188 |
-
b_new.click(show_new, outputs=[out,tab,np])
|
189 |
-
b_best.click(show_best,outputs=[out,tab,bp])
|
190 |
-
b_prev.click(prev, inputs=[tab,np,bp], outputs=[out,np,bp])
|
191 |
-
b_next.click(nxt, inputs=[tab,np,bp], outputs=[out,np,bp])
|
192 |
-
b_ref.click(lambda t,n,b: show_new(n)[0] if t=="new" else show_best(b)[0],
|
193 |
-
inputs=[tab,np,bp], outputs=[out])
|
194 |
-
|
195 |
-
demo.load(show_new, outputs=[out,tab,np])
|
196 |
-
return demo
|
197 |
-
|
198 |
-
app = build()
|
199 |
-
|
200 |
-
if __name__ == "__main__":
|
201 |
-
app.launch()
|
202 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|