Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -27,87 +27,203 @@ def _load_best():
|
|
27 |
print(f"BEST 데이터 로드 오류: {e}")
|
28 |
return []
|
29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
-
|
|
|
35 |
"""
|
36 |
-
Vercel API
|
|
|
37 |
"""
|
38 |
try:
|
|
|
|
|
|
|
|
|
|
|
39 |
# API 파라미터 설정
|
40 |
-
params = {"limit": limit}
|
41 |
if TEAM:
|
42 |
params["teamId"] = TEAM
|
43 |
|
44 |
-
print(f"Vercel API 호출 (
|
45 |
|
46 |
-
resp = requests.get(f"{API}/
|
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 "
|
57 |
-
print(f"API 응답에
|
58 |
return []
|
59 |
|
60 |
-
|
61 |
-
print(f"{len(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
|
63 |
-
# 각 프로젝트의 최신 배포 정보 가져오기
|
64 |
games = []
|
65 |
-
for
|
66 |
-
|
67 |
-
|
|
|
68 |
|
69 |
-
#
|
70 |
-
|
71 |
-
|
72 |
-
"limit": 1,
|
73 |
-
"projectId": project_id, # 중요: 해당 프로젝트의 배포만 필터링
|
74 |
-
"state": "READY"
|
75 |
-
}
|
76 |
|
77 |
-
|
78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
81 |
|
82 |
-
|
83 |
-
|
84 |
-
|
85 |
-
params=deploy_params,
|
86 |
-
timeout=30
|
87 |
-
)
|
88 |
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
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 |
-
# ─────────────────────
|
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 |
-
# ─────────────────────
|
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 |
-
# ─────────────────────
|
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()
|
|
|
|