ginipick commited on
Commit
4bfda21
ยท
verified ยท
1 Parent(s): 8b2bb99

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +306 -0
app.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()