ginipick commited on
Commit
2e9406d
·
verified ·
1 Parent(s): fa0ae19

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +106 -127
app.py CHANGED
@@ -1,175 +1,154 @@
1
- import os, time, json, datetime, requests, gradio as gr
2
 
3
  # ──────────────────────────────────────────────────────────
 
 
4
  # 1. Vercel API 설정
5
- VERCEL_API_TOKEN = os.getenv("SVR_TOKEN")
6
- VERCEL_TEAM_ID = os.getenv("VERCEL_TEAM_ID") # 개인 계정이면 None
7
- if not VERCEL_API_TOKEN:
8
  raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.")
 
 
9
 
10
- API_BASE = "https://api.vercel.com"
11
- HEADERS = {"Authorization": f"Bearer {VERCEL_API_TOKEN}"}
12
-
13
- # ──────────────────────────────────────────────────────────
14
- # 2. BEST 탭 파일 / 상수
15
- BEST_FILE = "best_games.json"
16
- CARDS_PER_PG = 48
17
-
18
- def init_best():
19
  if not os.path.exists(BEST_FILE):
20
  json.dump([], open(BEST_FILE, "w"))
21
-
22
- def load_best():
23
  try:
24
  return json.load(open(BEST_FILE))
25
  except Exception:
26
  return []
27
 
28
- # ──────────────────────────────────────────────────────────
29
- # 3. 모든 프로젝트 최신 배포 가져오기
30
- def fetch_deployments(limit=100):
31
  try:
32
  params = {"limit": limit}
33
- if VERCEL_TEAM_ID:
34
- params["teamId"] = VERCEL_TEAM_ID
35
- r = requests.get(f"{API_BASE}/v13/deployments",
36
- headers=HEADERS, params=params, timeout=30)
37
  r.raise_for_status()
38
 
39
- games = []
40
  for d in r.json().get("deployments", []):
41
  if d.get("state") != "READY":
42
  continue
43
- ts = int(d.get("createdAt", time.time()*1000) / 1000)
44
- games.append({
45
- "title" : d.get("name", "(제목 없음)"),
46
- "description": f"프로젝트: {d.get('name')}",
47
- "url" : f"https://{d.get('url')}",
48
- "timestamp" : ts,
 
49
  })
50
- return sorted(games, key=lambda x: x["timestamp"], reverse=True)
51
  except Exception as e:
52
  print("Vercel API 오류:", e)
53
  return []
54
 
55
- # ──────────────────────────────────────────────────────────
56
- def paginate(lst, page):
57
- s = (page-1) * CARDS_PER_PG
58
- e = s + CARDS_PER_PG
59
- total = (len(lst) + CARDS_PER_PG - 1)//CARDS_PER_PG
60
  return lst[s:e], total
61
 
62
- # ──────────────────────────────────────────────────────────
63
- # 4. HTML 생성 (파스텔 스타일, 가로 100 %)
64
- def make_html(cards, page, total):
65
  if not cards:
66
- return "<div style='text-align:center;padding:80px;font-size:1.1rem;color:#555;'>표시할 배포가 없습니다.</div>"
67
 
68
- css = """
69
  <style>
70
- body{margin:0;padding:0;font-family:'Poppins',sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
71
- .grid{display:grid;grid-template-columns:1fr;gap:40px;padding-bottom:60px}
72
- .card{background:#fff;border-radius:20px;overflow:hidden;box-shadow:0 15px 30px rgba(0,0,0,.08);transition:.3s}
73
- .card:hover{transform:translateY(-8px);box-shadow:0 25px 45px rgba(0,0,0,.12)}
74
- .header{padding:24px 28px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee}
75
- .title{margin:0;font-size:1.25rem;font-weight:700;color:#333}
76
- .date{font-size:.9rem;color:#777;margin-top:4px}
77
- .frame{position:relative;width:100%;padding-top:56.25%;overflow:hidden}
78
- .frame iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:0}
79
- .footer{padding:16px 28px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right}
80
- .link{font-size:.9rem;font-weight:600;color:#4a6dd8;text-decoration:none}
81
- .pager{display:flex;justify-content:center;gap:16px;margin:20px 0}
82
- .btn{border:none;background:#fff;padding:10px 24px;border-radius:12px;font-weight:600;
83
- box-shadow:0 4px 12px rgba(0,0,0,.05);cursor:pointer;transition:.25s}
84
- .btn:hover{background:#f1e8ff}
85
- .btn:disabled{opacity:.4;cursor:default}
86
- .count{font-size:.85rem;color:#555;text-align:center;margin-top:-4px}
87
  </style>
88
  """
89
-
90
- html = css + "<div class='grid'>"
91
  for c in cards:
92
- date = datetime.datetime.fromtimestamp(c["timestamp"]).strftime("%Y-%m-%d")
93
- html += f"""
94
  <div class='card'>
95
- <div class='header'>
96
- <p class='title'>{c['title']}</p>
97
- <p class='date'>{date}</p>
98
- </div>
99
- <div class='frame'>
100
- <iframe src="{c['url']}" loading="lazy"
101
- allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe>
102
- </div>
103
- <div class='footer'>
104
- <a href="{c['url']}" target="_blank" class='link'>원본 사이트 ↗</a>
105
- </div>
106
- </div>
107
- """
108
- html += "</div>"
109
- html += f"<p class='count'>Page {page} / {total}</p>"
110
- return html
111
-
112
- # ──────────────────────────────────────────────────────────
113
- def build_app():
114
- init_best()
115
-
116
  with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
117
- gr.Markdown("<h1 style='text-align:center;padding:30px 0;color:#333;'>🎮 Vibe Game Craft</h1>")
118
 
119
  with gr.Row():
120
- btn_new = gr.Button("NEW", size="sm")
121
- btn_best = gr.Button("BEST", size="sm")
122
- btn_prev = gr.Button("⬅️ Prev", size="sm")
123
- btn_next = gr.Button("Next ➡️", size="sm")
124
- btn_ref = gr.Button("🔄 Reload", size="sm")
125
-
126
- tab = gr.State("new")
127
- np = gr.State(1)
128
- bp = gr.State(1)
129
- out = gr.HTML()
130
-
131
- # 콜백 ────────────────────────────────────────────
132
  def show_new(p=1):
133
- cards, total = paginate(fetch_deployments(), p)
134
- return make_html(cards, p, total), "new", p
135
-
136
  def show_best(p=1):
137
- cards, total = paginate(load_best(), p)
138
- return make_html(cards, p, total), "best", p
139
 
140
- def prev(t, cp_n, cp_b):
 
141
  if t=="new":
142
- cp_n = max(1, cp_n-1); html, _,_ = show_new(cp_n)
143
- return html, cp_n, cp_b
144
- cp_b = max(1, cp_b-1); html, _,_ = show_best(cp_b)
145
- return html, cp_n, cp_b
146
-
147
- def nxt(t, cp_n, cp_b):
148
  if t=="new":
149
- max_pg = (len(fetch_deployments())+CARDS_PER_PG-1)//CARDS_PER_PG
150
- cp_n = min(max_pg, cp_n+1); html, _,_ = show_new(cp_n)
151
- return html, cp_n, cp_b
152
- max_pg = (len(load_best())+CARDS_PER_PG-1)//CARDS_PER_PG
153
- cp_b = min(max_pg, cp_b+1); html, _,_ = show_best(cp_b)
154
- return html, cp_n, cp_b
155
-
156
- def refresh(t, cp_n, cp_b):
157
- return show_new(cp_n)[0] if t=="new" else show_best(cp_b)[0]
158
-
159
- # ⦿ 버튼 연결
160
- btn_new.click(show_new, outputs=[out, tab, np])
161
- btn_best.click(show_best, outputs=[out, tab, bp])
162
- btn_prev.click(prev, inputs=[tab, np, bp], outputs=[out, np, bp])
163
- btn_next.click(nxt, inputs=[tab, np, bp], outputs=[out, np, bp])
164
- btn_ref.click(refresh, inputs=[tab, np, bp], outputs=[out])
165
-
166
- # 초기 로드
167
  demo.load(show_new, outputs=[out, tab, np])
168
 
169
  return demo
170
 
171
- # ──────────────────────────────────────────────────────────
172
- app = build_app()
173
 
174
  if __name__ == "__main__":
175
  app.launch()
 
 
1
 
2
  # ──────────────────────────────────────────────────────────
3
+ import os, time, json, datetime, requests, gradio as gr
4
+
5
  # 1. Vercel API 설정
6
+ TOKEN = os.getenv("SVR_TOKEN") # 필수
7
+ TEAM = os.getenv("VERCEL_TEAM_ID") # 계정이면 지정
8
+ if not TOKEN:
9
  raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.")
10
+ API = "https://api.vercel.com"
11
+ HEAD = {"Authorization": f"Bearer {TOKEN}"}
12
 
13
+ # 2. BEST 탭 데이터
14
+ BEST_FILE = "best_games.json"
15
+ PER_PAGE = 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
  return json.load(open(BEST_FILE))
22
  except Exception:
23
  return []
24
 
25
+ # 3. 모든 배포 가져오기 (v6)
26
+ def fetch_all(limit=200):
 
27
  try:
28
  params = {"limit": limit}
29
+ if TEAM:
30
+ params["teamId"] = TEAM
31
+ r = requests.get(f"{API}/v6/deployments",
32
+ headers=HEAD, params=params, timeout=30)
33
  r.raise_for_status()
34
 
35
+ out = []
36
  for d in r.json().get("deployments", []):
37
  if d.get("state") != "READY":
38
  continue
39
+ created_ms = d.get("created", time.time()*1000)
40
+ url_host = d.get("url", "")
41
+ url_full = url_host if url_host.startswith("http") else f"https://{url_host}"
42
+ out.append({
43
+ "title": d.get("name", "(제목 없음)"),
44
+ "url" : url_full,
45
+ "ts" : int(created_ms / 1000)
46
  })
47
+ return sorted(out, key=lambda x: x["ts"], reverse=True)
48
  except Exception as e:
49
  print("Vercel API 오류:", e)
50
  return []
51
 
52
+ # 4. 페이지네이션
53
+ def page(lst, pg):
54
+ s = (pg-1)*PER_PAGE
55
+ e = s + PER_PAGE
56
+ total = (len(lst)+PER_PAGE-1)//PER_PAGE
57
  return lst[s:e], total
58
 
59
+ # 5. HTML (3-열 그리드, 반응형)
60
+ def html(cards, pg, total):
 
61
  if not cards:
62
+ return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
63
 
64
+ css = r"""
65
  <style>
66
+ body{margin:0;font-family:Poppins,sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
67
+ .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px 24px;margin:0 20px 60px;}
68
+ @media(max-width:1024px){.grid{grid-template-columns:repeat(2,1fr);} }
69
+ @media(max-width:640px){ .grid{grid-template-columns:1fr;} }
70
+ .card{background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 10px 25px rgba(0,0,0,.08);transition:.3s}
71
+ .card:hover{transform:translateY(-6px);box-shadow:0 16px 40px rgba(0,0,0,.12)}
72
+ .hdr{padding:20px 24px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee;}
73
+ .ttl{margin:0;font-size:1.15rem;font-weight:700;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
74
+ .date{margin-top:4px;font-size:.85rem;color:#777;}
75
+ .frame{position:relative;width:100%;padding-top:60%;overflow:hidden;}
76
+ .frame iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
77
+ transform:scale(.7);transform-origin:top left;border:0;}
78
+ .foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
79
+ .link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
80
+ .cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 40px;}
 
 
81
  </style>
82
  """
83
+ h = css + "<div class='grid'>"
 
84
  for c in cards:
85
+ date = datetime.datetime.fromtimestamp(c["ts"]).strftime("%Y-%m-%d")
86
+ h += f"""
87
  <div class='card'>
88
+ <div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
89
+ <div class='frame'><iframe src="{c['url']}" loading="lazy"
90
+ allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
91
+ <div class='foot'><a class='link' href="{c['url']}" target="_blank">원본↗</a></div>
92
+ </div>"""
93
+ h += "</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
94
+ return h
95
+
96
+ # 6. Gradio UI
97
+ def build():
98
+ _init_best()
 
 
 
 
 
 
 
 
 
 
99
  with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
100
+ gr.Markdown("<h1 style='text-align:center;padding:32px 0 0;color:#333;'>🎮 Vibe Game Craft</h1>")
101
 
102
  with gr.Row():
103
+ b_new = gr.Button("NEW", size="sm")
104
+ b_best = gr.Button("BEST", size="sm")
105
+ b_prev = gr.Button("⬅️ Prev", size="sm")
106
+ b_next = gr.Button("Next ➡️", size="sm")
107
+ b_ref = gr.Button("🔄 Reload", size="sm")
108
+
109
+ tab = gr.State("new") # "new" / "best"
110
+ np = gr.State(1) # NEW 페이지
111
+ bp = gr.State(1) # BEST 페이지
112
+ out = gr.HTML()
113
+
114
+ # NEW / BEST 렌더
115
  def show_new(p=1):
116
+ data, tot = page(fetch_all(), p)
117
+ return html(data, p, tot), "new", p
 
118
  def show_best(p=1):
119
+ data, tot = page(_load_best(), p)
120
+ return html(data, p, tot), "best", p
121
 
122
+ # Prev / Next
123
+ def prev(t, n, b):
124
  if t=="new":
125
+ n=max(1,n-1); h,_,_ = show_new(n); return h, n, b
126
+ b=max(1,b-1); h,_,_ = show_best(b); return h, n, b
127
+ def nxt(t, n, b):
 
 
 
128
  if t=="new":
129
+ maxp=(len(fetch_all())+PER_PAGE-1)//PER_PAGE
130
+ n=min(maxp,n+1); h,_,_ = show_new(n); return h, n, b
131
+ maxp=(len(_load_best())+PER_PAGE-1)//PER_PAGE
132
+ b=min(maxp,b+1); h,_,_ = show_best(b); return h, n, b
133
+
134
+ # Reload
135
+ def reload(t, n, b):
136
+ return show_new(n)[0] if t=="new" else show_best(b)[0]
137
+
138
+ # 버튼 연결
139
+ b_new.click(show_new, outputs=[out, tab, np])
140
+ b_best.click(show_best, outputs=[out, tab, bp])
141
+ b_prev.click(prev, inputs=[tab, np, bp], outputs=[out, np, bp])
142
+ b_next.click(nxt, inputs=[tab, np, bp], outputs=[out, np, bp])
143
+ b_ref.click(reload, inputs=[tab, np, bp], outputs=[out])
144
+
145
+ # 초기 로드 — NEW 1페이지
 
146
  demo.load(show_new, outputs=[out, tab, np])
147
 
148
  return demo
149
 
150
+ # 7. 실행
151
+ app = build()
152
 
153
  if __name__ == "__main__":
154
  app.launch()