ginipick commited on
Commit
9cea90a
·
verified ·
1 Parent(s): 38c180a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +180 -58
app.py CHANGED
@@ -27,87 +27,203 @@ def _load_best():
27
  print(f"BEST 데이터 로드 오류: {e}")
28
  return []
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- def fetch_all(limit=50):
 
35
  """
36
- Vercel API v10/projects를 사용하여 프로젝트 목록을 가져옵니다.
 
37
  """
38
  try:
 
 
 
 
 
39
  # API 파라미터 설정
40
- params = {"limit": limit}
41
  if TEAM:
42
  params["teamId"] = TEAM
43
 
44
- print(f"Vercel API 호출 (프로젝트): {API}/v10/projects (params={params})")
45
 
46
- resp = requests.get(f"{API}/v10/projects",
47
  headers=HEAD, params=params, timeout=30)
48
 
49
- print(f"API 응답 상태 코드: {resp.status_code}")
50
  if resp.status_code != 200:
51
  print(f"API 응답 오류: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
52
  return []
53
 
54
  data = resp.json()
55
 
56
- if "projects" not in data:
57
- print(f"API 응답에 projects 필드가 없습니다: {str(data)[:200]}...")
58
  return []
59
 
60
- projects = data.get("projects", [])
61
- print(f"{len(projects)}개의 프로젝트를 찾았습니다")
 
 
 
 
 
 
 
62
 
63
- # 각 프로젝트의 최신 배포 정보 가져오기
64
  games = []
65
- for project in projects:
66
- project_id = project.get("id")
67
- project_name = project.get("name", "(제목 없음)")
 
68
 
69
- # 프로젝트별 최신 배포 가져오기 (projectId 필터 추가)
70
- try:
71
- deploy_params = {
72
- "limit": 1,
73
- "projectId": project_id, # 중요: 해당 프로젝트의 배포만 필터링
74
- "state": "READY"
75
- }
76
 
77
- if TEAM:
78
- deploy_params["teamId"] = TEAM
 
 
 
 
 
 
79
 
80
- print(f"프로젝트 {project_id} ({project_name}) 배포 조회 중...")
 
 
 
 
 
 
 
 
81
 
82
- deploy_resp = requests.get(
83
- f"{API}/v6/deployments",
84
- headers=HEAD,
85
- params=deploy_params,
86
- timeout=30
87
- )
88
 
89
- if deploy_resp.status_code == 200:
90
- deploy_data = deploy_resp.json()
91
- deployments = deploy_data.get("deployments", [])
92
-
93
- if deployments:
94
- deployment = deployments[0]
95
- url = deployment.get("url", "")
96
- if url:
97
- games.append({
98
- "title": project_name,
99
- "url": f"https://{url}",
100
- "ts": int(deployment.get("created", time.time() * 1000) / 1000),
101
- "projectId": project_id
102
- })
103
- print(f"프로젝트 {project_name}의 배포 URL: https://{url}")
104
- else:
105
- print(f"프로젝트 {project_name}에 배포된 버전이 없습니다.")
106
- except Exception as e:
107
- print(f"프로젝트 {project_id}의 배포 정보 가져오기 실패: {e}")
108
- continue
109
-
110
- print(f"총 {len(games)}개의 유효한 배포를 찾았습니다")
111
  return sorted(games, key=lambda x: x["ts"], reverse=True)
112
 
113
  except Exception as e:
@@ -116,13 +232,13 @@ def fetch_all(limit=50):
116
  traceback.print_exc()
117
  return []
118
 
119
- # ───────────────────── 4. 페이지네이션 ───────────────────
120
  def page(lst, pg):
121
  s=(pg-1)*PER_PAGE; e=s+PER_PAGE
122
  total=(len(lst)+PER_PAGE-1)//PER_PAGE
123
  return lst[s:e], total
124
 
125
- # ───────────────────── 5. HTML 3-열 그리드 ───────────────
126
  def html(cards, pg, total):
127
  if not cards:
128
  return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
@@ -158,7 +274,7 @@ def html(cards, pg, total):
158
  h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
159
  return h
160
 
161
- # ───────────────────── 6. Gradio Blocks UI ───────────────
162
  def build():
163
  _init_best()
164
  with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
@@ -169,11 +285,18 @@ def build():
169
  b_prev = gr.Button("⬅️ Prev", size="sm")
170
  b_next = gr.Button("Next ➡️", size="sm")
171
  b_ref = gr.Button("🔄 Reload", size="sm")
 
172
 
173
  tab = gr.State("new"); np = gr.State(1); bp = gr.State(1); out = gr.HTML()
174
 
175
  def show_new(p=1): d,t=page(fetch_all(),p); return html(d,p,t),"new",p
176
  def show_best(p=1): d,t=page(_load_best(),p); return html(d,p,t),"best",p
 
 
 
 
 
 
177
 
178
  def prev(t,n,b):
179
  if t=="new": n=max(1,n-1); h,_,_=show_new(n); return h,n,b
@@ -191,6 +314,7 @@ def build():
191
  b_next.click(nxt, inputs=[tab,np,bp], outputs=[out,np,bp])
192
  b_ref.click(lambda t,n,b: show_new(n)[0] if t=="new" else show_best(b)[0],
193
  inputs=[tab,np,bp], outputs=[out])
 
194
 
195
  demo.load(show_new, outputs=[out,tab,np])
196
  return demo
@@ -198,6 +322,4 @@ def build():
198
  app = build()
199
 
200
  if __name__ == "__main__":
201
- app.launch()
202
-
203
-
 
27
  print(f"BEST 데이터 로드 오류: {e}")
28
  return []
29
 
30
+ # ───────────────────── 3. 배포 보호 설정 관리 ─────────────────
31
+ def get_projects():
32
+ """Vercel 프로젝트 목록을 가져옵니다."""
33
+ try:
34
+ params = {"limit": 100} # 최대 100개 프로젝트
35
+ if TEAM:
36
+ params["teamId"] = TEAM
37
+
38
+ print(f"프로젝트 목록 가져오는 중...")
39
+ resp = requests.get(f"{API}/v10/projects", headers=HEAD, params=params)
40
+
41
+ if resp.status_code != 200:
42
+ print(f"프로젝트 목록 가져오기 실패: {resp.status_code}")
43
+ return []
44
+
45
+ projects = resp.json().get("projects", [])
46
+ print(f"{len(projects)}개의 프로젝트를 찾았습니다.")
47
+ return projects
48
+ except Exception as e:
49
+ print(f"프로젝트 목록 가져오기 오류: {e}")
50
+ return []
51
 
52
+ def disable_protection(project_id):
53
+ """특정 프로젝트의 배포 보호를 비활성화합니다."""
54
+ try:
55
+ url = f"{API}/v9/projects/{project_id}/protection"
56
+ payload = {
57
+ "protection": {
58
+ "preview": {
59
+ "enabled": False
60
+ },
61
+ "production": {
62
+ "enabled": False
63
+ }
64
+ }
65
+ }
66
+
67
+ print(f"프로젝트 {project_id}의 보호 설정 비활성화 중...")
68
+ resp = requests.put(url, headers=HEAD, json=payload)
69
+
70
+ if resp.status_code in [200, 201, 204]:
71
+ print(f"프로젝트 {project_id}의 보호 설정이 성공적으로 비활성화되었습니다.")
72
+ return True
73
+ else:
74
+ print(f"보호 설정 변경 실패: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
75
+ return False
76
+
77
+ except Exception as e:
78
+ print(f"보호 설정 변경 오류: {str(e)}")
79
+ return False
80
 
81
+ def add_exception_domain(project_id, domain):
82
+ """특정 프로젝트에 보호 예외 도메인을 추가합니다."""
83
+ try:
84
+ url = f"{API}/v9/projects/{project_id}/protection"
85
+
86
+ # 먼저 현재 보호 설정 가져오기
87
+ get_resp = requests.get(url, headers=HEAD)
88
+ if get_resp.status_code != 200:
89
+ print(f"보호 설정 가져오기 실패: {get_resp.status_code}")
90
+ return False
91
+
92
+ current = get_resp.json()
93
+
94
+ # 예외 도메인 목록에 추가
95
+ exceptions = current.get("protection", {}).get("preview", {}).get("exceptions", [])
96
+ if domain not in exceptions:
97
+ exceptions.append(domain)
98
+
99
+ # 업데이트된 설정 적용
100
+ payload = {
101
+ "protection": {
102
+ "preview": {
103
+ "enabled": True,
104
+ "exceptions": exceptions
105
+ }
106
+ }
107
+ }
108
+
109
+ print(f"프로젝트 {project_id}에 예외 도메인 {domain} 추가 중...")
110
+ resp = requests.put(url, headers=HEAD, json=payload)
111
+
112
+ if resp.status_code in [200, 201, 204]:
113
+ print(f"프로젝트 {project_id}에 예외 도메인이 성공적으로 추가되었습니다.")
114
+ return True
115
+ else:
116
+ print(f"예외 도메인 추가 실패: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
117
+ return False
118
+
119
+ except Exception as e:
120
+ print(f"예외 도메인 추가 오류: {str(e)}")
121
+ return False
122
 
123
+ def bypass_all_protection():
124
+ """모든 프로젝트의 배포 보호를 비활성화합니다."""
125
+ projects = get_projects()
126
+ if not projects:
127
+ print("프로젝트가 없거나 가져오지 못했습니다.")
128
+ return False
129
+
130
+ success_count = 0
131
+ for project in projects:
132
+ project_id = project.get("id")
133
+ name = project.get("name", "알 수 없음")
134
+ if project_id:
135
+ print(f"프로젝트 처리 중: {name} ({project_id})")
136
+ if disable_protection(project_id):
137
+ success_count += 1
138
+
139
+ print(f"총 {len(projects)}개 중 {success_count}개 프로젝트의 보호 설정을 비활성화했습니다.")
140
+ return success_count > 0
141
 
142
+ # ───────────────────── 4. 배포 목록 가져오기 ─────────────────
143
+ def fetch_all(limit=200, auto_bypass=True):
144
  """
145
+ Vercel API v6/deployments를 사용하여 모든 준비된 배포를 가져오고,
146
+ 필요에 따라 자동으로 보호 우회 설정을 적용합니다.
147
  """
148
  try:
149
+ # 자동 보호 우회 적용 (선택 사항)
150
+ if auto_bypass:
151
+ print("모든 프로젝트에 대한 보호 설정 자동 우회를 시도합니다...")
152
+ bypass_all_protection()
153
+
154
  # API 파라미터 설정
155
+ params = {"limit": limit, "state": "READY"}
156
  if TEAM:
157
  params["teamId"] = TEAM
158
 
159
+ print(f"Vercel API 호출 (배포): {API}/v6/deployments (params={params})")
160
 
161
+ resp = requests.get(f"{API}/v6/deployments",
162
  headers=HEAD, params=params, timeout=30)
163
 
 
164
  if resp.status_code != 200:
165
  print(f"API 응답 오류: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
166
  return []
167
 
168
  data = resp.json()
169
 
170
+ if "deployments" not in data:
171
+ print(f"API 응답에 deployments 필드가 없습니다: {str(data)[:200]}...")
172
  return []
173
 
174
+ deployments = data.get("deployments", [])
175
+ print(f"{len(deployments)}개의 배포를 찾았습니다")
176
+
177
+ # 도메인 패턴: vercel.app으로 끝나는지 확인
178
+ domain_pat = re.compile(r"\.vercel\.app$", re.I)
179
+
180
+ # 중복 방지를 위한 세트 (이미 처리한 URL)
181
+ processed_urls = set()
182
+ processed_projects = set()
183
 
 
184
  games = []
185
+ for deployment in deployments:
186
+ # URL 확인
187
+ url = deployment.get("url", "")
188
+ project_id = deployment.get("projectId")
189
 
190
+ # 이미 처리한 URL은 건너뜀
191
+ if url in processed_urls:
192
+ continue
 
 
 
 
193
 
194
+ processed_urls.add(url)
195
+
196
+ # vercel.app 도메인인지 확인
197
+ if url and domain_pat.search(url):
198
+ # 프로젝트별 보호 우회 설정 (각 프로젝트당 한 번만)
199
+ if auto_bypass and project_id and project_id not in processed_projects:
200
+ disable_protection(project_id)
201
+ processed_projects.add(project_id)
202
 
203
+ # 타임스탬프 처리
204
+ created_time = deployment.get("created")
205
+ if created_time:
206
+ try:
207
+ ts = int(created_time / 1000)
208
+ except:
209
+ ts = int(time.time())
210
+ else:
211
+ ts = int(time.time())
212
 
213
+ # 이름에 projectId가 있으면 보기 좋게 변환
214
+ name = deployment.get("name", "(제목 없음)")
215
+ name = re.sub(r"-[a-f0-9]{9}$", "", name) # projectId 부분 제거
 
 
 
216
 
217
+ games.append({
218
+ "title": name,
219
+ "url": f"https://{url}",
220
+ "ts": ts,
221
+ "projectId": project_id,
222
+ "deploymentId": deployment.get("uid", "")
223
+ })
224
+ print(f"배포 추가: {name} - https://{url}")
225
+
226
+ print(f"총 {len(games)}개의 유효한 배포를 필터링했습니다")
 
 
 
 
 
 
 
 
 
 
 
 
227
  return sorted(games, key=lambda x: x["ts"], reverse=True)
228
 
229
  except Exception as e:
 
232
  traceback.print_exc()
233
  return []
234
 
235
+ # ───────────────────── 5. 페이지네이션 ───────────────────
236
  def page(lst, pg):
237
  s=(pg-1)*PER_PAGE; e=s+PER_PAGE
238
  total=(len(lst)+PER_PAGE-1)//PER_PAGE
239
  return lst[s:e], total
240
 
241
+ # ───────────────────── 6. HTML 3-열 그리드 ───────────────
242
  def html(cards, pg, total):
243
  if not cards:
244
  return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
 
274
  h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
275
  return h
276
 
277
+ # ───────────────────── 7. Gradio Blocks UI ───────────────
278
  def build():
279
  _init_best()
280
  with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo:
 
285
  b_prev = gr.Button("⬅️ Prev", size="sm")
286
  b_next = gr.Button("Next ➡️", size="sm")
287
  b_ref = gr.Button("🔄 Reload", size="sm")
288
+ b_bypass = gr.Button("🔓 보호 해제", size="sm", variant="secondary")
289
 
290
  tab = gr.State("new"); np = gr.State(1); bp = gr.State(1); out = gr.HTML()
291
 
292
  def show_new(p=1): d,t=page(fetch_all(),p); return html(d,p,t),"new",p
293
  def show_best(p=1): d,t=page(_load_best(),p); return html(d,p,t),"best",p
294
+
295
+ def run_bypass():
296
+ if bypass_all_protection():
297
+ return "✅ 모든 프로젝트의 보호 설정이 비활성화되었습니다."
298
+ else:
299
+ return "❌ 일부 또는 모든 프로젝트의 보호 설정 비활성화에 실패했습니다."
300
 
301
  def prev(t,n,b):
302
  if t=="new": n=max(1,n-1); h,_,_=show_new(n); return h,n,b
 
314
  b_next.click(nxt, inputs=[tab,np,bp], outputs=[out,np,bp])
315
  b_ref.click(lambda t,n,b: show_new(n)[0] if t=="new" else show_best(b)[0],
316
  inputs=[tab,np,bp], outputs=[out])
317
+ b_bypass.click(run_bypass, outputs=[out])
318
 
319
  demo.load(show_new, outputs=[out,tab,np])
320
  return demo
 
322
  app = build()
323
 
324
  if __name__ == "__main__":
325
+ app.launch()