ginipick commited on
Commit
3573333
·
verified ·
1 Parent(s): 7f7f693

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +139 -219
app.py CHANGED
@@ -1,306 +1,226 @@
1
- import os
2
- import time
3
- import json
4
- import requests
5
- import base64
6
- import gradio as gr
7
- import datetime
8
 
9
- # Vercel API 설정
10
- VERCEL_API_TOKEN = os.getenv("SVR_TOKEN") # 환경 변수로부터 토큰을 읽음
 
11
  if not VERCEL_API_TOKEN:
12
  raise EnvironmentError("환경 변수 'SVR_TOKEN'이 설정되어 있지 않습니다!")
13
-
14
  VERCEL_API_URL = "https://api.vercel.com/v9"
15
  HEADERS = {
16
  "Authorization": f"Bearer {VERCEL_API_TOKEN}",
17
- "Content-Type": "application/json"
18
  }
19
 
20
- # 갤러리 설정
 
21
  BEST_GAMES_FILE = "best_games.json"
22
- GAMES_PER_PAGE = 48
23
- GAMES_PER_ROW = 3
24
 
25
- # BEST 게임 초기화
 
26
  def initialize_best_games():
27
  if not os.path.exists(BEST_GAMES_FILE):
28
- best_games = [
29
  {
30
  "title": "테트리스",
31
  "description": "클래식 테트리스 게임",
32
  "url": "https://tmkdop.vercel.app",
33
  "timestamp": time.time(),
34
- "preview_image": "https://via.placeholder.com/300x200?text=Tetris"
35
  },
36
  {
37
- "title": "스네이크 게임",
38
  "description": "전통적인 스네이크 게임",
39
  "url": "https://tmkdop.vercel.app",
40
  "timestamp": time.time(),
41
- "preview_image": "https://via.placeholder.com/300x200?text=Snake"
42
  },
43
  {
44
  "title": "팩맨",
45
- "description": "고전 아케이드 게임 팩맨",
46
  "url": "https://tmkdop.vercel.app",
47
  "timestamp": time.time(),
48
- "preview_image": "https://via.placeholder.com/300x200?text=Pacman"
49
- }
50
  ]
51
- with open(BEST_GAMES_FILE, "w") as f:
52
- json.dump(best_games, f)
53
- return load_best_games()
54
 
55
- # BEST 게임 로드
56
  def load_best_games():
57
  try:
58
- with open(BEST_GAMES_FILE, "r") as f:
59
- return json.load(f)
60
- except (FileNotFoundError, json.JSONDecodeError):
61
  return []
62
 
63
- # Vercel API에서 최신 배포 목록 가져오기
 
64
  def get_latest_deployments():
65
  try:
66
- response = requests.get(
67
  f"{VERCEL_API_URL}/deployments",
68
  headers=HEADERS,
69
- params={"limit": 100}
 
70
  )
71
- if response.status_code != 200:
72
- print(f"Vercel API 오류: {response.status_code} - {response.text}")
73
- return []
74
-
75
- deployments = response.json().get("deployments", [])
76
  games = []
77
- for deployment in deployments:
78
- if deployment.get("state") != "READY":
79
  continue
80
-
81
- created_at = deployment.get("createdAt")
82
- timestamp = int(time.time())
83
- try:
84
- # Vercel은 ms 단위 int를 반환하므로 이를 처리
85
- if isinstance(created_at, (int, float)):
86
- timestamp = int(created_at / 1000)
87
- elif isinstance(created_at, str):
88
- # Python 3.7+ 에서 ISO 파싱
89
- if hasattr(datetime.datetime, "fromisoformat"):
90
- dt = datetime.datetime.fromisoformat(created_at.replace("Z", "+00:00"))
91
- else:
92
- dt = datetime.datetime.strptime(
93
- created_at.split(".")[0], "%Y-%m-%dT%H:%M:%S"
94
- )
95
- timestamp = int(dt.timestamp())
96
- except Exception as e:
97
- print(f"날짜 변환 오류: {str(e)}")
98
-
99
  games.append(
100
  {
101
- "title": deployment.get("name", "게임"),
102
- "description": f"배포된 게임: {deployment.get('name')}",
103
- "url": f"https://{deployment.get('url')}",
104
- "timestamp": timestamp,
105
- "preview_image": (
106
- f"https://via.placeholder.com/300x200?text="
107
- f"{deployment.get('name', 'Game').replace(' ', '+')}"
108
- ),
109
  }
110
  )
111
-
112
- games.sort(key=lambda x: x["timestamp"], reverse=True)
113
- return games
114
  except Exception as e:
115
- print(f"배포 목록 가져오기 오류: {str(e)}")
116
  return []
117
 
118
- # 페이지네이션
119
- def paginate_games(games, page=1):
120
- start_idx = (page - 1) * GAMES_PER_PAGE
121
- end_idx = start_idx + GAMES_PER_PAGE
122
- total_pages = (len(games) + GAMES_PER_PAGE - 1) // GAMES_PER_PAGE
123
- return games[start_idx:end_idx], total_pages
124
-
125
- # 갤러리 HTML 생성
126
- def generate_gallery_html(games, current_page=1, total_pages=1, gallery_type="NEW"):
 
 
127
  if not games:
128
  return (
129
- "<div style='text-align:center;padding:50px;color:#666;background:white;"
130
- "border-radius:10px;box-shadow:0 2px 10px rgba(0,0,0,0.1);'>"
131
- "<h3>아직 게임이 없습니다</h3>"
132
- f"<p>{gallery_type} 탭에 표시할 게임이 없습니다.</p></div>"
133
  )
134
 
135
- html = """
 
136
  <style>
137
- .gallery-container{font-family:'Poppins',-apple-system,BlinkMacSystemFont,sans-serif;}
138
- .gallery-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-bottom:30px;}
139
- .game-card{background:white;border-radius:12px;overflow:hidden;box-shadow:0 4px 15px rgba(0,0,0,0.1);
140
- transition:transform .3s ease,box-shadow .3s ease;cursor:pointer;}
141
- .game-card:hover{transform:translateY(-8px);box-shadow:0 10px 25px rgba(0,0,0,0.15);}
142
- .game-preview{position:relative;height:200px;overflow:hidden;}
143
- .game-preview img{width:100%;height:100%;object-fit:cover;transition:transform .5s ease;}
144
- .game-card:hover .game-preview img{transform:scale(1.05);}
145
- .game-preview-overlay{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.3);
146
- display:flex;align-items:center;justify-content:center;opacity:0;
147
- transition:opacity .3s ease;}
148
- .game-card:hover .game-preview-overlay{opacity:1;}
149
- .play-btn{background-color:#9c89b8;color:white;padding:8px 16px;border-radius:50px;text-decoration:none;
150
- font-weight:500;font-size:14px;transition:background-color .3s ease;}
151
- .play-btn:hover{background-color:#f0a6ca;}
152
- .game-info{padding:20px;}
153
- .game-info h3{margin:0 0 10px;font-size:18px;color:#333;}
154
- .game-desc{color:#666;margin-bottom:15px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;
155
- overflow:hidden;font-size:14px;line-height:1.5;}
156
- .game-meta{display:flex;justify-content:space-between;align-items:center;font-size:13px;}
157
- .game-date{color:#888;}
158
- .game-pagination{display:flex;justify-content:center;gap:10px;margin-top:30px;}
159
- .page-btn{background-color:white;border:1px solid #ddd;color:#333;padding:8px 16px;border-radius:8px;
160
- cursor:pointer;transition:all .3s ease;font-size:14px;}
161
- .page-btn:hover,.page-btn.active{background-color:#9c89b8;color:white;border-color:#9c89b8;}
162
- @media (max-width:768px){.gallery-grid{grid-template-columns:repeat(2,1fr);}}
163
- @media (max-width:480px){.gallery-grid{grid-template-columns:1fr;}}
164
  </style>
165
- <div class="gallery-container"><div class="gallery-grid">
166
  """
167
 
168
- for game in games:
169
- formatted_date = datetime.datetime.fromtimestamp(game["timestamp"]).strftime("%Y-%m-%d")
 
170
  html += f"""
171
- <div class="game-card">
172
- <div class="game-preview">
173
- <img src="{game['preview_image']}" alt="{game['title']}">
174
- <div class="game-preview-overlay">
175
- <a href="{game['url']}" target="_blank" class="play-btn">플레이하기</a>
176
- </div>
177
- </div>
178
- <div class="game-info">
179
- <h3><a href="{game['url']}" target="_blank" style="text-decoration:none;color:inherit;">{game['title']}</a></h3>
180
- <p class="game-desc">{game['description']}</p>
181
- <div class="game-meta">
182
- <span class="game-date">{formatted_date}</span>
183
- <a href="{game['url']}" target="_blank" class="play-btn">플레이</a>
184
- </div>
185
- </div>
186
  </div>
187
  """
188
 
 
189
  html += "</div>"
190
-
191
  if total_pages > 1:
192
- html += '<div class="game-pagination">'
193
- if current_page > 1:
 
 
 
 
 
194
  html += (
195
- f'<button class="page-btn prev" '
196
- f'onclick="refreshGallery(\'{gallery_type.lower()}\',{current_page-1})">이전</button>'
197
  )
198
- start_page = max(1, current_page - 2)
199
- end_page = min(total_pages, start_page + 4)
200
- for i in range(start_page, end_page + 1):
201
- if i == current_page:
202
- html += f'<button class="page-btn active">{i}</button>'
203
- else:
204
- html += (
205
- f'<button class="page-btn" '
206
- f'onclick="refreshGallery(\'{gallery_type.lower()}\',{i})">{i}</button>'
207
- )
208
- if current_page < total_pages:
209
  html += (
210
- f'<button class="page-btn next" '
211
- f'onclick="refreshGallery(\'{gallery_type.lower()}\',{current_page+1})">다음</button>'
212
  )
213
  html += "</div>"
 
 
214
  html += """
215
  <script>
216
- function refreshGallery(type,page){
217
- if(type==='new'){document.getElementById('new-btn').click();}
218
- else{document.getElementById('best-btn').click();}
219
- }
220
- </script></div>"""
221
  return html
222
 
223
- # 탭 인터페이스
 
224
  def create_gallery_interface():
225
  initialize_best_games()
226
 
227
- css = """
228
- .gallery-header{text-align:center;margin-bottom:30px;}
229
- .gallery-header h1{color:#333;font-size:2.5rem;margin-bottom:10px;}
230
- .gallery-header p{color:#666;font-size:1.1rem;}
231
- .tabs-container{margin-bottom:30px;}
232
- .tab-buttons{display:flex;gap:10px;margin-bottom:20px;}
233
- .tab-button{padding:10px 20px;background-color:#f0f0f0;border:none;border-radius:8px;cursor:pointer;
234
- font-size:16px;font-weight:500;transition:all .3s ease;}
235
- .tab-button.active{background-color:#9c89b8;color:white;}
236
- .refresh-btn{background-color:#b8bedd;color:white;border:none;border-radius:8px;padding:8px 16px;cursor:pointer;
237
- font-size:14px;margin-left:10px;transition:background-color .3s ease;}
238
- .refresh-btn:hover{background-color:#9c89b8;}
239
- """
240
 
241
- with gr.Blocks(css=css) as demo:
242
- gr.HTML(
243
- "<div class='gallery-header'><h1>🎮 Vibe Game Craft 갤러리</h1>"
244
- "<p>AI로 생성된 게임들을 탐색하고 플레이해보세요</p></div>"
245
- )
246
 
247
- with gr.Row(elem_id="tab-buttons", elem_classes="tab-buttons"):
248
- new_tab_btn = gr.Button("NEW", elem_classes="tab-button active", elem_id="new-btn")
249
- best_tab_btn = gr.Button("BEST", elem_classes="tab-button", elem_id="best-btn")
250
- refresh_btn = gr.Button("🔄 새로고침", elem_classes="refresh-btn")
251
 
252
- current_tab = gr.State("new")
253
- new_page = gr.State(1)
254
- best_page = gr.State(1)
255
- gallery_html = gr.HTML(elem_id="gallery-content")
256
 
257
- # NEW 탭
258
- def show_new_tab():
259
- games = get_latest_deployments()
260
- page_games, total_pages = paginate_games(games, 1)
261
- return generate_gallery_html(page_games, 1, total_pages, "NEW"), "new", 1
262
 
263
- new_tab_btn.click(
264
- fn=show_new_tab,
265
- outputs=[gallery_html, current_tab, new_page]
266
- )
267
 
268
- # BEST 탭
269
- def show_best_tab():
270
- games = load_best_games()
271
- page_games, total_pages = paginate_games(games, 1)
272
- return generate_gallery_html(page_games, 1, total_pages, "BEST"), "best", 1
273
 
274
- best_tab_btn.click(
275
- fn=show_best_tab,
276
- outputs=[gallery_html, current_tab, best_page]
277
- )
278
 
279
- # 새로고침
280
- def refresh_gallery(current_tab_state, new_page_state, best_page_state):
281
- if current_tab_state == "new":
282
- games = get_latest_deployments()
283
- page_games, total_pages = paginate_games(games, new_page_state)
284
- return generate_gallery_html(page_games, new_page_state, total_pages, "NEW")
285
- games = load_best_games()
286
- page_games, total_pages = paginate_games(games, best_page_state)
287
- return generate_gallery_html(page_games, best_page_state, total_pages, "BEST")
288
 
289
- refresh_btn.click(
290
- fn=refresh_gallery,
291
- inputs=[current_tab, new_page, best_page],
292
- outputs=[gallery_html]
293
- )
294
 
295
- # 초기 로드
296
- demo.load(
297
- fn=show_new_tab,
298
- outputs=[gallery_html, current_tab, new_page]
299
- )
300
 
301
  return demo
302
 
303
- # 실행
 
 
 
304
  if __name__ == "__main__":
305
- gallery = create_gallery_interface()
306
- gallery.launch()
 
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
  {
25
  "title": "테트리스",
26
  "description": "클래식 테트리스 게임",
27
  "url": "https://tmkdop.vercel.app",
28
  "timestamp": time.time(),
 
29
  },
30
  {
31
+ "title": "스네이크",
32
  "description": "전통적인 스네이크 게임",
33
  "url": "https://tmkdop.vercel.app",
34
  "timestamp": time.time(),
 
35
  },
36
  {
37
  "title": "팩맨",
38
+ "description": "고전 아케이드 게임",
39
  "url": "https://tmkdop.vercel.app",
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
  {
73
+ "title": d.get("name", "게임"),
74
+ "description": f"배포된 게임: {d.get('name')}",
75
+ "url": f"https://{d.get('url')}",
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")
123
  html += f"""
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
+ # ────────────────────────────────────────────────────────────────────────────────
168
+ # 7. Gradio UI
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
189
 
190
+ def render_best(page=1):
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__":
226
+ app.launch()