Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,14 +1,23 @@
|
|
1 |
-
|
2 |
import os, re, time, json, datetime, requests, gradio as gr
|
3 |
|
4 |
# ───────────────────── 1. Vercel API ─────────────────────
|
5 |
TOKEN = os.getenv("SVR_TOKEN")
|
6 |
TEAM = os.getenv("VERCEL_TEAM_ID")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
if not TOKEN:
|
8 |
raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.")
|
|
|
9 |
API = "https://api.vercel.com"
|
10 |
HEAD = {"Authorization": f"Bearer {TOKEN}"}
|
11 |
|
|
|
|
|
12 |
# ───────────────────── 2. BEST 데이터 ────────────────────
|
13 |
BEST_FILE, PER_PAGE = "best_games.json", 48
|
14 |
def _init_best():
|
@@ -24,11 +33,9 @@ def _load_best():
|
|
24 |
except Exception:
|
25 |
return []
|
26 |
|
27 |
-
# ───────────────────── 3. NEW:
|
28 |
-
#
|
29 |
-
|
30 |
-
# 수정
|
31 |
-
PAT = re.compile(r"^[a-z0-9-]{1,}\.vercel\.app$", re.I) # 6자 제한 완화
|
32 |
|
33 |
def fetch_all(limit=200):
|
34 |
"""
|
@@ -41,14 +48,21 @@ def fetch_all(limit=200):
|
|
41 |
return []
|
42 |
|
43 |
params = {"limit": limit}
|
|
|
|
|
44 |
if TEAM:
|
45 |
params["teamId"] = TEAM
|
|
|
|
|
|
|
46 |
|
47 |
-
print(f"Vercel API 호출: {API}/v6/deployments (
|
48 |
|
49 |
resp = requests.get(f"{API}/v6/deployments",
|
50 |
headers=HEAD, params=params, timeout=30)
|
51 |
|
|
|
|
|
52 |
if resp.status_code != 200:
|
53 |
print(f"API 응답 오류: {resp.status_code}, {resp.text[:200]}")
|
54 |
return []
|
@@ -138,36 +152,54 @@ def html(cards, pg, total):
|
|
138 |
background:#f8f8f8;color:#666;font-size:14px;text-align:center;
|
139 |
padding:20px;box-sizing:border-box;flex-direction:column;}
|
140 |
.frame-error-icon {font-size:36px;margin-bottom:12px;color:#999;}
|
|
|
|
|
|
|
|
|
141 |
</style>
|
142 |
|
143 |
<script>
|
144 |
// iframe 로드 오류 처리 함수
|
145 |
function handleIframeError(iframe) {
|
146 |
const container = iframe.parentNode;
|
|
|
147 |
iframe.style.display = 'none';
|
148 |
const errorDiv = document.createElement('div');
|
149 |
errorDiv.className = 'frame-error';
|
150 |
errorDiv.innerHTML = '<div class="frame-error-icon">🔒</div>' +
|
151 |
-
'<div
|
152 |
-
'<div
|
153 |
-
'<
|
154 |
container.appendChild(errorDiv);
|
155 |
}
|
156 |
|
157 |
// iframe 로드 성공 처리
|
158 |
function handleIframeLoad(iframe) {
|
159 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
160 |
}
|
161 |
|
162 |
// iframe 로드 상태 확인
|
163 |
document.addEventListener('DOMContentLoaded', function() {
|
164 |
-
// 모든 iframe에
|
165 |
const frames = document.querySelectorAll('iframe');
|
166 |
frames.forEach(frame => {
|
|
|
167 |
frame.addEventListener('error', function() {
|
168 |
handleIframeError(this);
|
169 |
});
|
170 |
|
|
|
|
|
|
|
|
|
|
|
171 |
// 5초 후에도 로드되지 않으면 오류로 간주
|
172 |
setTimeout(function() {
|
173 |
if(frame.style.opacity !== '1') {
|
@@ -186,8 +218,8 @@ def html(cards, pg, total):
|
|
186 |
<div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
|
187 |
<div class='frame'>
|
188 |
<iframe src="{c['url']}" loading="lazy"
|
189 |
-
|
190 |
-
|
191 |
</div>
|
192 |
<div class='foot'><a class='link' href="{c['url']}" target="_blank">원본↗</a></div>
|
193 |
</div>"""
|
@@ -234,4 +266,4 @@ def build():
|
|
234 |
app = build()
|
235 |
|
236 |
if __name__ == "__main__":
|
237 |
-
app.launch()
|
|
|
|
|
1 |
import os, re, time, json, datetime, requests, gradio as gr
|
2 |
|
3 |
# ───────────────────── 1. Vercel API ─────────────────────
|
4 |
TOKEN = os.getenv("SVR_TOKEN")
|
5 |
TEAM = os.getenv("VERCEL_TEAM_ID")
|
6 |
+
|
7 |
+
# 팀 ID가 없는 경우 여기에 직접 입력할 수 있음
|
8 |
+
if not TEAM:
|
9 |
+
# 실제 팀 ID로 교체하세요
|
10 |
+
TEAM = "team_YOUR_ACTUAL_TEAM_ID_HERE"
|
11 |
+
print(f"환경 변수에 팀 ID가 없어서 하드코딩된 값을 사용합니다: {TEAM}")
|
12 |
+
|
13 |
if not TOKEN:
|
14 |
raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.")
|
15 |
+
|
16 |
API = "https://api.vercel.com"
|
17 |
HEAD = {"Authorization": f"Bearer {TOKEN}"}
|
18 |
|
19 |
+
print(f"사용 중인 팀 ID: {TEAM if TEAM else '없음'}")
|
20 |
+
|
21 |
# ───────────────────── 2. BEST 데이터 ────────────────────
|
22 |
BEST_FILE, PER_PAGE = "best_games.json", 48
|
23 |
def _init_best():
|
|
|
33 |
except Exception:
|
34 |
return []
|
35 |
|
36 |
+
# ───────────────────── 3. NEW: Vercel.app 배포 ─────
|
37 |
+
# 패턴 완화: 모든 vercel.app 도메인 허용
|
38 |
+
PAT = re.compile(r"^[a-z0-9-]{1,}\.vercel\.app$", re.I)
|
|
|
|
|
39 |
|
40 |
def fetch_all(limit=200):
|
41 |
"""
|
|
|
48 |
return []
|
49 |
|
50 |
params = {"limit": limit}
|
51 |
+
|
52 |
+
# 팀 ID 처리 개선
|
53 |
if TEAM:
|
54 |
params["teamId"] = TEAM
|
55 |
+
print(f"팀 ID로 요청합니다: {TEAM}")
|
56 |
+
else:
|
57 |
+
print("경고: 팀 ID가 설정되지 않았습니다. 개인 프로젝트만 조회됩니다.")
|
58 |
|
59 |
+
print(f"Vercel API 호출: {API}/v6/deployments (params={params})")
|
60 |
|
61 |
resp = requests.get(f"{API}/v6/deployments",
|
62 |
headers=HEAD, params=params, timeout=30)
|
63 |
|
64 |
+
# 상세한 응답 정보 출력
|
65 |
+
print(f"API 응답 상태 코드: {resp.status_code}")
|
66 |
if resp.status_code != 200:
|
67 |
print(f"API 응답 오류: {resp.status_code}, {resp.text[:200]}")
|
68 |
return []
|
|
|
152 |
background:#f8f8f8;color:#666;font-size:14px;text-align:center;
|
153 |
padding:20px;box-sizing:border-box;flex-direction:column;}
|
154 |
.frame-error-icon {font-size:36px;margin-bottom:12px;color:#999;}
|
155 |
+
.frame-error-btn {margin-top:12px;padding:6px 16px;background:#4a6dd8;color:white;
|
156 |
+
border-radius:20px;font-size:12px;cursor:pointer;border:none;
|
157 |
+
transition:all 0.2s ease;}
|
158 |
+
.frame-error-btn:hover {background:#3a5dc8;transform:scale(1.05);}
|
159 |
</style>
|
160 |
|
161 |
<script>
|
162 |
// iframe 로드 오류 처리 함수
|
163 |
function handleIframeError(iframe) {
|
164 |
const container = iframe.parentNode;
|
165 |
+
const url = iframe.src;
|
166 |
iframe.style.display = 'none';
|
167 |
const errorDiv = document.createElement('div');
|
168 |
errorDiv.className = 'frame-error';
|
169 |
errorDiv.innerHTML = '<div class="frame-error-icon">🔒</div>' +
|
170 |
+
'<div><strong>접근이 제한된 배포입니다</strong></div>' +
|
171 |
+
'<div>팀 설정으로 인해 iframe에서 표시할 수 없습니다</div>' +
|
172 |
+
'<button class="frame-error-btn" onclick="window.open(\'' + url + '\', \'_blank\')">새 탭에서 열기</button>';
|
173 |
container.appendChild(errorDiv);
|
174 |
}
|
175 |
|
176 |
// iframe 로드 성공 처리
|
177 |
function handleIframeLoad(iframe) {
|
178 |
+
try {
|
179 |
+
// CORS 오류 탐지 시도
|
180 |
+
iframe.contentWindow.location.href;
|
181 |
+
iframe.style.opacity = 1;
|
182 |
+
} catch (e) {
|
183 |
+
// CORS 오류가 발생하면 handleIframeError 호출
|
184 |
+
handleIframeError(iframe);
|
185 |
+
}
|
186 |
}
|
187 |
|
188 |
// iframe 로드 상태 확인
|
189 |
document.addEventListener('DOMContentLoaded', function() {
|
190 |
+
// 모든 iframe에 핸들러 추가
|
191 |
const frames = document.querySelectorAll('iframe');
|
192 |
frames.forEach(frame => {
|
193 |
+
// 오류 이벤트 리스너
|
194 |
frame.addEventListener('error', function() {
|
195 |
handleIframeError(this);
|
196 |
});
|
197 |
|
198 |
+
// 로드 이벤트 리스너
|
199 |
+
frame.addEventListener('load', function() {
|
200 |
+
handleIframeLoad(this);
|
201 |
+
});
|
202 |
+
|
203 |
// 5초 후에도 로드되지 않으면 오류로 간주
|
204 |
setTimeout(function() {
|
205 |
if(frame.style.opacity !== '1') {
|
|
|
218 |
<div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
|
219 |
<div class='frame'>
|
220 |
<iframe src="{c['url']}" loading="lazy"
|
221 |
+
sandbox="allow-scripts allow-same-origin"
|
222 |
+
allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe>
|
223 |
</div>
|
224 |
<div class='foot'><a class='link' href="{c['url']}" target="_blank">원본↗</a></div>
|
225 |
</div>"""
|
|
|
266 |
app = build()
|
267 |
|
268 |
if __name__ == "__main__":
|
269 |
+
app.launch()
|