import os, re, time, json, datetime, requests, gradio as gr # ───────────────────── 1. Vercel API ───────────────────── TOKEN = os.getenv("SVR_TOKEN") TEAM = os.getenv("VERCEL_TEAM_ID") TEAM_SLUG = os.getenv("VERCEL_TEAM_SLUG") # 팀 슬러그도 추가 # 팀 ID 또는 슬러그 확인 if not TEAM and TEAM_SLUG: print(f"팀 ID 대신 팀 슬러그를 사용합니다: {TEAM_SLUG}") elif not TEAM: # 실제 팀 ID로 교체하세요 TEAM = "team_YOUR_ACTUAL_TEAM_ID_HERE" print(f"환경 변수에 팀 ID가 없어서 하드코딩된 값을 사용합니다: {TEAM}") if not TOKEN: raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.") API = "https://api.vercel.com" HEAD = {"Authorization": f"Bearer {TOKEN}"} print(f"사용 중인 팀 정보: ID={TEAM if TEAM else '없음'}, Slug={TEAM_SLUG if TEAM_SLUG else '없음'}") # ───────────────────── 2. BEST 데이터 ──────────────────── BEST_FILE, PER_PAGE = "best_games.json", 48 def _init_best(): if not os.path.exists(BEST_FILE): json.dump([], open(BEST_FILE, "w")) def _load_best(): try: data = json.load(open(BEST_FILE)) for it in data: if "ts" not in it: it["ts"] = int(it.get("timestamp", time.time())) return data except Exception: return [] # ───────────────────── 3. NEW: Vercel.app 배포 ───── # 패턴 완화: 모든 vercel.app 도메인 허용 PAT = re.compile(r"^[a-z0-9-]{1,}\.vercel\.app$", re.I) def fetch_all(limit=200): """ Vercel 배포 중 vercel.app 도메인을 가진 것을 수집 """ try: # 토큰 검증 if not TOKEN: print("오류: API 토큰이 설정되지 않았습니다") return [] # API 문서 기반으로 파라미터 설정 params = { "limit": limit, # 상태가 "READY"인 배포만 가져오기 위한 필터링 "state": "READY" } # 팀 ID 또는 슬러그 처리 if TEAM: params["teamId"] = TEAM print(f"팀 ID로 요청합니다: {TEAM}") elif TEAM_SLUG: params["slug"] = TEAM_SLUG print(f"팀 슬러그로 요청합니다: {TEAM_SLUG}") else: print("경고: 팀 정보가 설정되지 않았습니다. 개인 프로젝트만 조회됩니다.") print(f"Vercel API 호출: {API}/v6/deployments (params={params})") resp = requests.get(f"{API}/v6/deployments", headers=HEAD, params=params, timeout=30) # 상세한 응답 정보 출력 print(f"API 응답 상태 코드: {resp.status_code}") if resp.status_code != 200: error_msg = f"API 응답 오류: {resp.status_code}" if hasattr(resp, 'text'): error_msg += f", {resp.text[:200]}" print(error_msg) return [] data = resp.json() if "deployments" not in data: print(f"API 응답에 deployments 필드가 없습니다: {str(data)[:200]}...") return [] print(f"{len(data['deployments'])}개의 배포를 찾았습니다") # 디버깅: 첫 번째 응답 형식 확인 if data["deployments"]: first = data["deployments"][0] print(f"\n첫 번째 배포 URL: {first.get('url')}") print(f"첫 번째 배포 Alias: {first.get('alias', [])}\n") print(f"첫 번째 배포 상태: {first.get('state')}\n") # 패턴 완화: vercel.app으로 끝나는 모든 URL 허용 domain_pat = re.compile(r"\.vercel\.app$", re.I) games = [] for d in data.get("deployments", []): # 문서에서는 state 필터링을 URL 파라미터로 했지만, # 추가 검증을 위해 여기서도 확인합니다 if d.get("state") != "READY": continue # URL이나 alias 중 하나라도 vercel.app으로 끝나면 포함 url = d.get("url", "") aliases = d.get("alias", []) matched_url = None if url and domain_pat.search(url): matched_url = url else: for alias in aliases: if domain_pat.search(alias): matched_url = alias break if matched_url: # 시간 형식 변환 개선 created_time = d.get("created") if created_time: # milliseconds to seconds 변환 try: ts = int(created_time / 1000) except: ts = int(time.time()) else: ts = int(time.time()) games.append({ "title": d.get("name", "(제목 없음)"), "url": f"https://{matched_url}", "ts": ts, "meta": { # 추가 메타데이터 저장 "projectId": d.get("projectId"), "target": d.get("target"), "deploymentId": d.get("uid") } }) print(f"vercel.app 도메인 배포 {len(games)}개를 필터링했습니다") return sorted(games, key=lambda x: x["ts"], reverse=True) except Exception as e: print(f"Vercel API 오류: {str(e)}") import traceback traceback.print_exc() return [] # ───────────────────── 4. 페이지네이션 ─────────────────── def page(lst, pg): s=(pg-1)*PER_PAGE; e=s+PER_PAGE total=(len(lst)+PER_PAGE-1)//PER_PAGE return lst[s:e], total # ───────────────────── 5. HTML 3-열 그리드 ─────────────── def html(cards, pg, total): if not cards: return "
표시할 배포가 없습니다.
" css=r""" """ h=css+"
" for c in cards: date=datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d") h+=f"""

{c['title']}

{date}

""" h+="

Page "+str(pg)+" / "+str(total)+"

" return h # ───────────────────── 6. Gradio Blocks UI ─────────────── def build(): _init_best() with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;}") as demo: gr.Markdown("

🎮 Vibe Game Craft

") with gr.Row(): b_new = gr.Button("NEW", size="sm") b_best = gr.Button("BEST", size="sm") b_prev = gr.Button("⬅️ Prev", size="sm") b_next = gr.Button("Next ➡️", size="sm") b_ref = gr.Button("🔄 Reload", size="sm") tab = gr.State("new"); np = gr.State(1); bp = gr.State(1); out = gr.HTML() def show_new(p=1): d,t=page(fetch_all(),p); return html(d,p,t),"new",p def show_best(p=1): d,t=page(_load_best(),p); return html(d,p,t),"best",p def prev(t,n,b): if t=="new": n=max(1,n-1); h,_,_=show_new(n); return h,n,b b=max(1,b-1); h,_,_=show_best(b); return h,n,b def nxt(t,n,b): if t=="new": maxp=(len(fetch_all())+PER_PAGE-1)//PER_PAGE n=min(maxp,n+1); h,_,_=show_new(n); return h,n,b maxp=(len(_load_best())+PER_PAGE-1)//PER_PAGE b=min(maxp,b+1); h,_,_=show_best(b); return h,n,b b_new.click(show_new, outputs=[out,tab,np]) b_best.click(show_best,outputs=[out,tab,bp]) b_prev.click(prev, inputs=[tab,np,bp], outputs=[out,np,bp]) b_next.click(nxt, inputs=[tab,np,bp], outputs=[out,np,bp]) b_ref.click(lambda t,n,b: show_new(n)[0] if t=="new" else show_best(b)[0], inputs=[tab,np,bp], outputs=[out]) demo.load(show_new, outputs=[out,tab,np]) return demo app = build() if __name__ == "__main__": app.launch()