ginipick commited on
Commit
2312f14
·
verified ·
1 Parent(s): f002f12

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +103 -93
app.py CHANGED
@@ -1,24 +1,37 @@
1
- import os, time, json, requests, datetime, gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
  # ────────────────────────────────────────────────────────────────────────────────
4
- # 1. Vercel API 설정 ─ 환경변수에서 토큰 읽기
5
- VERCEL_API_TOKEN = os.getenv("SVR_TOKEN")
6
  if not VERCEL_API_TOKEN:
7
  raise EnvironmentError("환경 변수 'SVR_TOKEN'이 설정되어 있지 않습니다!")
8
- VERCEL_API_URL = "https://api.vercel.com/v9"
9
  HEADERS = {
10
  "Authorization": f"Bearer {VERCEL_API_TOKEN}",
11
  "Content-Type": "application/json",
12
  }
13
 
14
  # ────────────────────────────────────────────────────────────────────────────────
15
- # 2. 갤러리·페이지네이션 기본값
16
  BEST_GAMES_FILE = "best_games.json"
17
  GAMES_PER_PAGE = 48
18
 
19
  # ────────────────────────────────────────────────────────────────────────────────
20
- # 3. BEST 탭용 샘플 게임 초기화
21
- def initialize_best_games():
22
  if not os.path.exists(BEST_GAMES_FILE):
23
  sample = [
24
  {
@@ -40,33 +53,32 @@ def initialize_best_games():
40
  "timestamp": time.time(),
41
  },
42
  ]
43
- json.dump(sample, open(BEST_GAMES_FILE, "w"))
44
 
45
- def load_best_games():
46
  try:
47
  return json.load(open(BEST_GAMES_FILE))
48
  except Exception:
49
  return []
50
 
51
  # ────────────────────────────────────────────────────────────────────────────────
52
- # 4. 최신 Vercel 배포 → 게임 목록
53
- def get_latest_deployments():
54
  try:
55
- r = requests.get(
56
  f"{VERCEL_API_URL}/deployments",
57
  headers=HEADERS,
58
  params={"limit": 100},
59
  timeout=30,
60
  )
61
- r.raise_for_status()
62
  games = []
63
- for d in r.json().get("deployments", []):
64
  if d.get("state") != "READY":
65
  continue
66
- ts = (
67
- int(d["createdAt"] / 1000)
68
- if isinstance(d["createdAt"], (int, float))
69
- else int(time.time())
70
  )
71
  games.append(
72
  {
@@ -76,47 +88,44 @@ def get_latest_deployments():
76
  "timestamp": ts,
77
  }
78
  )
79
- return sorted(games, key=lambda x: x["timestamp"], reverse=True)
 
80
  except Exception as e:
81
  print("Vercel API 오류:", e)
82
  return []
83
 
84
  # ────────────────────────────────────────────────────────────────────────────────
85
- # 5. 페이지네이션 헬퍼
86
- def paginate(lst, page):
87
  start = (page - 1) * GAMES_PER_PAGE
88
  end = start + GAMES_PER_PAGE
89
  total = (len(lst) + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
90
  return lst[start:end], total
91
 
92
  # ────────────────────────────────────────────────────────────────────────────────
93
- # 6. 갤러리 HTML (사이트 미러링 스타일)
94
- def generate_gallery_html(games, page, total_pages, tab_name):
95
  if not games:
96
- return (
97
- "<div style='text-align:center;padding:60px;'>표시할 게임이 없습니다.</div>"
98
- )
99
 
100
- # CSS (필요 최소만 인라인 - reference 스타일 재구성)
101
  css = """
102
  <style>
103
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:24px;margin-bottom:40px}
104
- .item{background:white;border-radius:14px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,.1);transition:.3s}
105
  .item:hover{transform:translateY(-6px);box-shadow:0 12px 30px rgba(0,0,0,.15)}
106
  .hdr{padding:14px 18px;background:rgba(255,255,255,.8);backdrop-filter:blur(6px);border-bottom:1px solid #eee}
107
- .ttl{font-size:1.1rem;font-weight:700;margin:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
108
- .meta{font-size:.85rem;color:#666;margin-top:4px;display:flex;justify-content:space-between;align-items:center}
109
  .iframe-box{position:relative;width:100%;padding-top:60%;overflow:hidden}
110
  .iframe-box iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
111
  transform:scale(.7);transform-origin:top left;border:0}
112
  .footer{padding:10px 16px;background:rgba(255,255,255,.85);backdrop-filter:blur(6px);text-align:right}
113
  .footer a{font-size:.85rem;font-weight:600;color:#2c5282;text-decoration:none}
114
  .pg{display:flex;justify-content:center;gap:10px;margin-top:10px}
115
- .pg button{border:1px solid #ddd;background:white;padding:6px 14px;border-radius:8px;cursor:pointer}
116
- .pg button.active,.pg button:hover{background:#9c89b8;color:white;border-color:#9c89b8}
117
  </style>
118
  """
119
-
120
  html = css + "<div class='grid'>"
121
  for g in games:
122
  date = datetime.datetime.fromtimestamp(g["timestamp"]).strftime("%Y-%m-%d")
@@ -124,44 +133,16 @@ def generate_gallery_html(games, page, total_pages, tab_name):
124
  <div class='item'>
125
  <div class='hdr'>
126
  <p class='ttl'>{g['title']}</p>
127
- <div class='meta'><span>{date}</span></div>
128
- </div>
129
- <div class='iframe-box'>
130
- <iframe src="{g['url']}" loading="lazy"
131
- allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe>
132
- </div>
133
- <div class='footer'>
134
- <a href="{g['url']}" target="_blank">원본 사이트 열기 ↗</a>
135
  </div>
 
 
 
136
  </div>
137
  """
138
-
139
- # 페이지네이션
140
  html += "</div>"
141
- if total_pages > 1:
142
- html += "<div class='pg'>"
143
- if page > 1:
144
- html += (
145
- f"<button onclick=\"refreshGallery('{tab_name}',{page-1})\">이전</button>"
146
- )
147
- for p in range(max(1, page - 2), min(total_pages, page + 2) + 1):
148
- active = "active" if p == page else ""
149
- html += (
150
- f"<button class='{active}' onclick=\"refreshGallery('{tab_name}',{p})\">{p}</button>"
151
- )
152
- if page < total_pages:
153
- html += (
154
- f"<button onclick=\"refreshGallery('{tab_name}',{page+1})\">다음</button>"
155
- )
156
- html += "</div>"
157
-
158
- # refreshGallery JS (탭 버튼 재사용)
159
- html += """
160
- <script>
161
- function refreshGallery(t,p){document.getElementById(t+'-btn').click();
162
- window.setTimeout(()=>window.__setPage&&__setPage(t,p),50);}
163
- </script>
164
- """
165
  return html
166
 
167
  # ────────────────────────────────────────────────────────────────────────────────
@@ -169,20 +150,23 @@ def generate_gallery_html(games, page, total_pages, tab_name):
169
  def create_gallery_interface():
170
  initialize_best_games()
171
 
172
- with gr.Blocks() as demo:
173
  gr.HTML("<h1 style='text-align:center;'>🎮 Vibe Game Craft</h1>")
174
 
175
  with gr.Row():
176
  new_btn = gr.Button("NEW", elem_id="new-btn")
177
  best_btn = gr.Button("BEST", elem_id="best-btn")
178
  refresh = gr.Button("🔄 새로고침")
 
 
179
 
180
- tab_state = gr.State("new")
181
- page_state = gr.State(1) # NEW 현재 페이지
182
- page_state2 = gr.State(1) # BEST 탭 현재 페이지
183
- gallery_out = gr.HTML()
 
184
 
185
- # 각 렌더 함수
186
  def render_new(page=1):
187
  games, total = paginate(get_latest_deployments(), page)
188
  return generate_gallery_html(games, page, total, "new"), "new", page
@@ -191,35 +175,61 @@ def create_gallery_interface():
191
  games, total = paginate(load_best_games(), page)
192
  return generate_gallery_html(games, page, total, "best"), "best", page
193
 
194
- new_btn.click(fn=render_new, outputs=[gallery_out, tab_state, page_state])
195
- best_btn.click(fn=render_best, outputs=[gallery_out, tab_state, page_state2])
 
196
 
197
- # 새로고침
198
- def do_refresh(tab, p1, p2):
199
- if tab == "new":
200
- return render_new(p1)[0]
201
- return render_best(p2)[0]
202
 
203
- refresh.click(fn=do_refresh,
204
- inputs=[tab_state, page_state, page_state2],
205
- outputs=[gallery_out])
206
-
207
- # 페이지네이션을 위한 JS 콜백용 숨은 함수
208
- def set_page(tab, p):
209
  if tab == "new":
210
- return render_new(p)
211
- return render_best(p)
212
 
213
- # 초기 NEW
214
- demo.load(fn=render_new, outputs=[gallery_out, tab_state, page_state])
 
215
 
216
- # JS 에서 호출될 수 있도록 함수 노출
217
- demo.set_event_trigger("__setPage", set_page, [tab_state, page_state, page_state2], [gallery_out, tab_state, page_state, page_state2])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  return demo
220
 
221
  # ────────────────────────────────────────────────────────────────────────────────
222
- # 8. 실행 (Gradio Spaces 자동 감지를 위해 전역 app 변수 노출)
223
  app = create_gallery_interface()
224
 
225
  if __name__ == "__main__":
 
1
+ """
2
+ Vibe Game Craft – NEW / BEST 탭을 ‘사이트 미러링’(iframe) 방식으로 보여주는 Gradio Space
3
+
4
+ ● Gradio 4.x 공용 API만 사용 – set_event_trigger 제거
5
+ ● Prev / Next 버튼으로 페이지 전환
6
+ ● os.getenv("SVR_TOKEN") 필수
7
+ """
8
+
9
+ import os
10
+ import time
11
+ import json
12
+ import requests
13
+ import datetime
14
+ import gradio as gr
15
 
16
  # ────────────────────────────────────────────────────────────────────────────────
17
+ # 1. Vercel API 설정
18
+ VERCEL_API_TOKEN = os.getenv("SVR_TOKEN") # ← 환경변수에서 읽음
19
  if not VERCEL_API_TOKEN:
20
  raise EnvironmentError("환경 변수 'SVR_TOKEN'이 설정되어 있지 않습니다!")
21
+ VERCEL_API_URL = "https://api.vercel.com/v9"
22
  HEADERS = {
23
  "Authorization": f"Bearer {VERCEL_API_TOKEN}",
24
  "Content-Type": "application/json",
25
  }
26
 
27
  # ────────────────────────────────────────────────────────────────────────────────
28
+ # 2. 갤러리 설정
29
  BEST_GAMES_FILE = "best_games.json"
30
  GAMES_PER_PAGE = 48
31
 
32
  # ────────────────────────────────────────────────────────────────────────────────
33
+ # 3. BEST 초기화 / 로드
34
+ def initialize_best_games() -> None:
35
  if not os.path.exists(BEST_GAMES_FILE):
36
  sample = [
37
  {
 
53
  "timestamp": time.time(),
54
  },
55
  ]
56
+ json.dump(sample, open(BEST_GAMES_FILE, "w"), ensure_ascii=False, indent=2)
57
 
58
+ def load_best_games() -> list:
59
  try:
60
  return json.load(open(BEST_GAMES_FILE))
61
  except Exception:
62
  return []
63
 
64
  # ────────────────────────────────────────────────────────────────────────────────
65
+ # 4. Vercel 최신 배포 가져오기
66
+ def get_latest_deployments() -> list:
67
  try:
68
+ resp = requests.get(
69
  f"{VERCEL_API_URL}/deployments",
70
  headers=HEADERS,
71
  params={"limit": 100},
72
  timeout=30,
73
  )
74
+ resp.raise_for_status()
75
  games = []
76
+ for d in resp.json().get("deployments", []):
77
  if d.get("state") != "READY":
78
  continue
79
+ created_at = d.get("createdAt", time.time() * 1000)
80
+ ts = int(created_at / 1000) if isinstance(created_at, (int, float)) else int(
81
+ time.time()
 
82
  )
83
  games.append(
84
  {
 
88
  "timestamp": ts,
89
  }
90
  )
91
+ games.sort(key=lambda x: x["timestamp"], reverse=True)
92
+ return games
93
  except Exception as e:
94
  print("Vercel API 오류:", e)
95
  return []
96
 
97
  # ────────────────────────────────────────────────────────────────────────────────
98
+ # 5. 페이지네이션
99
+ def paginate(lst: list, page: int):
100
  start = (page - 1) * GAMES_PER_PAGE
101
  end = start + GAMES_PER_PAGE
102
  total = (len(lst) + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
103
  return lst[start:end], total
104
 
105
  # ────────────────────────────────────────────────────────────────────────────────
106
+ # 6. 갤러리 HTML (iframe 미러링)
107
+ def generate_gallery_html(games: list, page: int, total_pages: int, tab_name: str) -> str:
108
  if not games:
109
+ return "<div style='text-align:center;padding:60px;'>표시할 게임이 없습니다.</div>"
 
 
110
 
 
111
  css = """
112
  <style>
113
  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:24px;margin-bottom:40px}
114
+ .item{background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,.1);transition:.3s}
115
  .item:hover{transform:translateY(-6px);box-shadow:0 12px 30px rgba(0,0,0,.15)}
116
  .hdr{padding:14px 18px;background:rgba(255,255,255,.8);backdrop-filter:blur(6px);border-bottom:1px solid #eee}
117
+ .ttl{margin:0;font-size:1.1rem;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
118
+ .meta{font-size:.85rem;color:#666;margin-top:4px}
119
  .iframe-box{position:relative;width:100%;padding-top:60%;overflow:hidden}
120
  .iframe-box iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
121
  transform:scale(.7);transform-origin:top left;border:0}
122
  .footer{padding:10px 16px;background:rgba(255,255,255,.85);backdrop-filter:blur(6px);text-align:right}
123
  .footer a{font-size:.85rem;font-weight:600;color:#2c5282;text-decoration:none}
124
  .pg{display:flex;justify-content:center;gap:10px;margin-top:10px}
125
+ .pg button{border:1px solid #ddd;background:#fff;padding:6px 14px;border-radius:8px;cursor:pointer}
126
+ .pg button.active,.pg button:hover{background:#9c89b8;color:#fff;border-color:#9c89b8}
127
  </style>
128
  """
 
129
  html = css + "<div class='grid'>"
130
  for g in games:
131
  date = datetime.datetime.fromtimestamp(g["timestamp"]).strftime("%Y-%m-%d")
 
133
  <div class='item'>
134
  <div class='hdr'>
135
  <p class='ttl'>{g['title']}</p>
136
+ <div class='meta'>{date}</div>
 
 
 
 
 
 
 
137
  </div>
138
+ <div class='iframe-box'><iframe src="{g['url']}" loading="lazy"
139
+ allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
140
+ <div class='footer'><a href="{g['url']}" target="_blank">원본 사이트 열기 ↗</a></div>
141
  </div>
142
  """
 
 
143
  html += "</div>"
144
+ # 페이지 번호(읽기 전용) – Prev/Next 는 Gradio에서 처리
145
+ html += f"<div style='text-align:center;margin-top:4px;font-size:.85rem;color:#666;'>Page {page} / {total_pages}</div>"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  return html
147
 
148
  # ────────────────────────────────────────────────────────────────────────────────
 
150
  def create_gallery_interface():
151
  initialize_best_games()
152
 
153
+ with gr.Blocks(title="Vibe Game Craft") as demo:
154
  gr.HTML("<h1 style='text-align:center;'>🎮 Vibe Game Craft</h1>")
155
 
156
  with gr.Row():
157
  new_btn = gr.Button("NEW", elem_id="new-btn")
158
  best_btn = gr.Button("BEST", elem_id="best-btn")
159
  refresh = gr.Button("🔄 새로고침")
160
+ prev_btn = gr.Button("⬅️ Prev")
161
+ next_btn = gr.Button("Next ➡️")
162
 
163
+ # 상태
164
+ current_tab = gr.State("new") # "new" or "best"
165
+ new_page = gr.State(1)
166
+ best_page = gr.State(1)
167
+ gallery_html = gr.HTML()
168
 
169
+ # NEW / BEST 렌더 함수
170
  def render_new(page=1):
171
  games, total = paginate(get_latest_deployments(), page)
172
  return generate_gallery_html(games, page, total, "new"), "new", page
 
175
  games, total = paginate(load_best_games(), page)
176
  return generate_gallery_html(games, page, total, "best"), "best", page
177
 
178
+ # 버튼 이벤트
179
+ new_btn.click(fn=render_new,
180
+ outputs=[gallery_html, current_tab, new_page])
181
 
182
+ best_btn.click(fn=render_best,
183
+ outputs=[gallery_html, current_tab, best_page])
 
 
 
184
 
185
+ def refresh_gallery(tab, p_new, p_best):
 
 
 
 
 
186
  if tab == "new":
187
+ return render_new(p_new)[0]
188
+ return render_best(p_best)[0]
189
 
190
+ refresh.click(fn=refresh_gallery,
191
+ inputs=[current_tab, new_page, best_page],
192
+ outputs=[gallery_html])
193
 
194
+ # Prev / Next
195
+ def prev_page(tab, p_new, p_best):
196
+ if tab == "new":
197
+ p_new = max(1, p_new - 1)
198
+ html, _, _ = render_new(p_new)
199
+ return html, p_new, p_best
200
+ p_best = max(1, p_best - 1)
201
+ html, _, _ = render_best(p_best)
202
+ return html, p_new, p_best
203
+
204
+ def next_page(tab, p_new, p_best):
205
+ if tab == "new":
206
+ games_total = len(get_latest_deployments())
207
+ max_pages = (games_total + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
208
+ p_new = min(max_pages, p_new + 1)
209
+ html, _, _ = render_new(p_new)
210
+ return html, p_new, p_best
211
+ games_total = len(load_best_games())
212
+ max_pages = (games_total + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
213
+ p_best = min(max_pages, p_best + 1)
214
+ html, _, _ = render_best(p_best)
215
+ return html, p_new, p_best
216
+
217
+ prev_btn.click(fn=prev_page,
218
+ inputs=[current_tab, new_page, best_page],
219
+ outputs=[gallery_html, new_page, best_page])
220
+
221
+ next_btn.click(fn=next_page,
222
+ inputs=[current_tab, new_page, best_page],
223
+ outputs=[gallery_html, new_page, best_page])
224
+
225
+ # 초기 NEW 탭 표시
226
+ demo.load(fn=render_new,
227
+ outputs=[gallery_html, current_tab, new_page])
228
 
229
  return demo
230
 
231
  # ────────────────────────────────────────────────────────────────────────────────
232
+ # 8. 전역 app (Spaces 자동 감지)
233
  app = create_gallery_interface()
234
 
235
  if __name__ == "__main__":