Spaces:
Running
Running
Update app.py
Browse files
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 |
-
#
|
10 |
-
|
|
|
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
|
23 |
-
GAMES_PER_ROW = 3
|
24 |
|
25 |
-
#
|
|
|
26 |
def initialize_best_games():
|
27 |
if not os.path.exists(BEST_GAMES_FILE):
|
28 |
-
|
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 |
-
|
49 |
-
}
|
50 |
]
|
51 |
-
|
52 |
-
json.dump(best_games, f)
|
53 |
-
return load_best_games()
|
54 |
|
55 |
-
# BEST 게임 로드
|
56 |
def load_best_games():
|
57 |
try:
|
58 |
-
|
59 |
-
|
60 |
-
except (FileNotFoundError, json.JSONDecodeError):
|
61 |
return []
|
62 |
|
63 |
-
#
|
|
|
64 |
def get_latest_deployments():
|
65 |
try:
|
66 |
-
|
67 |
f"{VERCEL_API_URL}/deployments",
|
68 |
headers=HEADERS,
|
69 |
-
params={"limit": 100}
|
|
|
70 |
)
|
71 |
-
|
72 |
-
print(f"Vercel API 오류: {response.status_code} - {response.text}")
|
73 |
-
return []
|
74 |
-
|
75 |
-
deployments = response.json().get("deployments", [])
|
76 |
games = []
|
77 |
-
for
|
78 |
-
if
|
79 |
continue
|
80 |
-
|
81 |
-
|
82 |
-
|
83 |
-
|
84 |
-
|
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":
|
102 |
-
"description": f"배포된 게임: {
|
103 |
-
"url": f"https://{
|
104 |
-
"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(
|
116 |
return []
|
117 |
|
118 |
-
#
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
124 |
-
|
125 |
-
|
126 |
-
|
|
|
|
|
127 |
if not games:
|
128 |
return (
|
129 |
-
"<div style='text-align:center;padding:
|
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 |
-
|
|
|
136 |
<style>
|
137 |
-
.
|
138 |
-
.
|
139 |
-
.
|
140 |
-
|
141 |
-
.
|
142 |
-
.
|
143 |
-
.
|
144 |
-
.
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
.
|
149 |
-
.
|
150 |
-
|
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 |
-
|
169 |
-
|
|
|
170 |
html += f"""
|
171 |
-
<div class=
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
-
|
176 |
-
|
177 |
-
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
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 +=
|
193 |
-
if
|
|
|
|
|
|
|
|
|
|
|
194 |
html += (
|
195 |
-
f
|
196 |
-
f'onclick="refreshGallery(\'{gallery_type.lower()}\',{current_page-1})">이전</button>'
|
197 |
)
|
198 |
-
|
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
|
211 |
-
f'onclick="refreshGallery(\'{gallery_type.lower()}\',{current_page+1})">다음</button>'
|
212 |
)
|
213 |
html += "</div>"
|
|
|
|
|
214 |
html += """
|
215 |
<script>
|
216 |
-
function refreshGallery(
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
</script></div>"""
|
221 |
return html
|
222 |
|
223 |
-
#
|
|
|
224 |
def create_gallery_interface():
|
225 |
initialize_best_games()
|
226 |
|
227 |
-
|
228 |
-
|
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 |
-
|
242 |
-
|
243 |
-
|
244 |
-
"
|
245 |
-
)
|
246 |
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
|
252 |
-
|
253 |
-
|
254 |
-
|
255 |
-
|
256 |
|
257 |
-
|
258 |
-
|
259 |
-
games
|
260 |
-
page_games, total_pages = paginate_games(games, 1)
|
261 |
-
return generate_gallery_html(page_games, 1, total_pages, "NEW"), "new", 1
|
262 |
|
263 |
-
|
264 |
-
|
265 |
-
outputs=[gallery_html, current_tab, new_page]
|
266 |
-
)
|
267 |
|
268 |
-
#
|
269 |
-
def
|
270 |
-
|
271 |
-
|
272 |
-
return
|
273 |
|
274 |
-
|
275 |
-
|
276 |
-
|
277 |
-
)
|
278 |
|
279 |
-
#
|
280 |
-
def
|
281 |
-
if
|
282 |
-
|
283 |
-
|
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 |
-
|
290 |
-
|
291 |
-
inputs=[current_tab, new_page, best_page],
|
292 |
-
outputs=[gallery_html]
|
293 |
-
)
|
294 |
|
295 |
-
#
|
296 |
-
demo.
|
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 |
-
|
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()
|
|