ginipick commited on
Commit
f22af82
·
verified ·
1 Parent(s): 293f472

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -184
app.py CHANGED
@@ -1,21 +1,13 @@
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
- if not TOKEN:
7
- raise EnvironmentError("SVR_TOKEN 환경변수를 설정하세요.")
8
- API = "https://api.vercel.com"
9
- HEAD = {"Authorization": f"Bearer {TOKEN}"}
10
-
11
- print(f"API 토큰: {TOKEN[:4]}... (마스킹됨)")
12
- print(f"팀 ID: {TEAM if TEAM else '없음'}")
13
 
14
  # ───────────────────── 2. BEST 데이터 ────────────────────
15
- BEST_FILE, PER_PAGE = "best_games.json", 9
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
  data = json.load(open(BEST_FILE))
@@ -71,189 +63,160 @@ def add_url_to_best(title, url):
71
  print(f"URL 추가 오류: {str(e)}")
72
  return False
73
 
74
- # ───────────────────── 4. 배포 보호 설정 관리 ─────────────────
75
- def get_projects():
76
- """Vercel 프로젝트 목록을 가져옵니다."""
77
- try:
78
- params = {"limit": 100} # 최대 100개 프로젝트
79
- if TEAM:
80
- params["teamId"] = TEAM
81
-
82
- print(f"프로젝트 목록 가져오는 중...")
83
- resp = requests.get(f"{API}/v10/projects", headers=HEAD, params=params)
84
-
85
- if resp.status_code != 200:
86
- print(f"프로젝트 목록 가져오기 실패: {resp.status_code}")
87
- return []
88
-
89
- projects = resp.json().get("projects", [])
90
- print(f"{len(projects)}개의 프로젝트를 찾았습니다.")
91
- return projects
92
- except Exception as e:
93
- print(f"프로젝트 목록 가져오기 오류: {e}")
94
- return []
95
-
96
- def disable_protection(project_id):
97
- """특정 프로젝트의 배포 보호를 비활성화합니다."""
98
- try:
99
- url = f"{API}/v9/projects/{project_id}/protection"
100
- payload = {
101
- "protection": {
102
- "preview": {
103
- "enabled": False
104
- },
105
- "production": {
106
- "enabled": False
107
- }
108
- }
109
- }
110
-
111
- print(f"프로젝트 {project_id}의 보호 설정 비활성화 중...")
112
- resp = requests.put(url, headers=HEAD, json=payload)
113
-
114
- if resp.status_code in [200, 201, 204]:
115
- print(f"프로젝트 {project_id}의 보호 설정이 성공적으로 비활성화되었습니다.")
116
- return True
117
- else:
118
- print(f"보호 설정 변경 실패: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
119
- return False
120
-
121
- except Exception as e:
122
- print(f"보호 설정 변경 오류: {str(e)}")
123
- return False
124
-
125
- def add_exception_domain(project_id, domain):
126
- """특정 프로젝트에 보호 예외 도메인을 추가합니다."""
127
- try:
128
- url = f"{API}/v9/projects/{project_id}/protection"
129
-
130
- # 먼저 현재 보호 설정 가져오기
131
- get_resp = requests.get(url, headers=HEAD)
132
- if get_resp.status_code != 200:
133
- print(f"보호 설정 가져오기 실패: {get_resp.status_code}")
134
- return False
135
-
136
- current = get_resp.json()
137
-
138
- # 예외 도메인 목록에 추가
139
- exceptions = current.get("protection", {}).get("preview", {}).get("exceptions", [])
140
- if domain not in exceptions:
141
- exceptions.append(domain)
142
-
143
- # 업데이트된 설정 적용
144
- payload = {
145
- "protection": {
146
- "preview": {
147
- "enabled": True,
148
- "exceptions": exceptions
149
- }
150
- }
151
- }
152
-
153
- print(f"프로젝트 {project_id}에 예외 도메인 {domain} 추가 중...")
154
- resp = requests.put(url, headers=HEAD, json=payload)
155
-
156
- if resp.status_code in [200, 201, 204]:
157
- print(f"프로젝트 {project_id}에 예외 도메인이 성공적으로 추가되었습니다.")
158
- return True
159
- else:
160
- print(f"예외 도메인 추가 실패: {resp.status_code}, {resp.text[:200] if hasattr(resp, 'text') else ''}")
161
- return False
162
-
163
- except Exception as e:
164
- print(f"예외 도메인 추가 오류: {str(e)}")
165
- return False
166
-
167
- def bypass_all_protection():
168
- """모든 프로젝트의 배포 보호를 비활성화합니다."""
169
- projects = get_projects()
170
- if not projects:
171
- print("프로젝트가 없거나 가져오지 못했습니다.")
172
- return False
173
-
174
- success_count = 0
175
- for project in projects:
176
- project_id = project.get("id")
177
- name = project.get("name", "알 수 없음")
178
- if project_id:
179
- print(f"프로젝트 처리 중: {name} ({project_id})")
180
- if disable_protection(project_id):
181
- success_count += 1
182
-
183
- print(f"총 {len(projects)}개 중 {success_count}개 프로젝트의 보호 설정을 비활성화했습니다.")
184
- return success_count > 0
185
-
186
- # ───────────────────── 5. 페이지네이션 ───────────────────
187
  def page(lst, pg):
188
- s=(pg-1)*PER_PAGE; e=s+PER_PAGE
189
- total=(len(lst)+PER_PAGE-1)//PER_PAGE
 
190
  return lst[s:e], total
191
 
192
- # ───────────────────── 6. HTML 3-열 그리드 ───────────────
193
  def html(cards, pg, total):
194
  if not cards:
195
  return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
196
- css=r"""
197
  <style>
198
- body{margin:0;font-family:Poppins,sans-serif;background:linear-gradient(135deg,#C5E8FF 0%,#FFD6E0 100%);}
199
- .grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px 24px;margin:0 auto;max-width:1200px;padding:20px;}
200
- @media(max-width:1024px){.grid{grid-template-columns:repeat(3,1fr);} }
201
- @media(max-width:768px){.grid{grid-template-columns:repeat(2,1fr);} }
202
- @media(max-width:500px){ .grid{grid-template-columns:1fr;} }
203
- .card{background:#fff;border-radius:18px;overflow:hidden;box-shadow:0 10px 25px rgba(0,0,0,.08);transition:.3s}
204
- .card:hover{transform:translateY(-6px);box-shadow:0 16px 40px rgba(0,0,0,.12)}
205
- .hdr{padding:20px 24px;background:rgba(255,255,255,.75);backdrop-filter:blur(8px);border-bottom:1px solid #eee;}
206
- .ttl{margin:0;font-size:1.15rem;font-weight:700;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
207
- .date{margin-top:4px;font-size:.85rem;color:#777;}
208
- .frame{position:relative;width:100%;padding-top:60%;overflow:hidden;}
209
- .frame iframe{position:absolute;top:0;left:0;width:142.857%;height:142.857%;
210
- transform:scale(.7);transform-origin:top left;border:0;}
211
- .foot{padding:14px 24px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);text-align:right;}
212
- .link{font-size:.85rem;font-weight:600;color:#4a6dd8;text-decoration:none;}
213
- .cnt{text-align:center;font-size:.85rem;color:#555;margin:10px 0 20px;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  </style>"""
215
 
216
- h=css+"<div class='grid'>"
217
  for c in cards:
218
- date=datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d")
219
- h+=f"""
220
  <div class='card'>
221
  <div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
222
  <div class='frame'><iframe src="{c['url']}" loading="lazy"
223
  allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
224
  <div class='foot'><a class='link' href="{c['url']}" target="_blank">원본↗</a></div>
225
  </div>"""
226
- h+="</div><p class='cnt'>Page "+str(pg)+" / "+str(total)+"</p>"
 
 
 
 
227
  return h
228
 
229
- # ───────────────────── 7. Gradio Blocks UI ───────────────
230
  def build():
231
  _init_best()
232
- with gr.Blocks(title="Vibe Game Craft", css="body{overflow-x:hidden;} .gradio-container{margin:0 auto; max-width:1280px;}") as demo:
233
- gr.Markdown("<h1 style='text-align:center;padding:32px 0 0;color:#333;'>🎮 Vibe Game Craft</h1>")
234
-
235
- # URL 추가 인터페이스
236
  with gr.Row():
237
- title_input = gr.Textbox(label="제목", placeholder="게임 제목을 입력하세요")
238
  url_input = gr.Textbox(label="URL", placeholder="https://...")
239
- add_btn = gr.Button("URL 추가", variant="primary")
240
 
241
- # 페이지 네비게이션
242
- with gr.Row():
243
- b_best = gr.Button("BEST", size="sm")
244
- b_prev = gr.Button("⬅️ Prev", size="sm")
245
- b_next = gr.Button("Next ➡️", size="sm")
246
- b_ref = gr.Button("🔄 Reload", size="sm")
247
- b_bypass = gr.Button("🔓 보호 해제", size="sm", variant="secondary")
248
-
249
  # 상태 및 출력
250
  bp = gr.State(1)
251
  out = gr.HTML()
252
- status_msg = gr.Textbox(label="상태 메시지", interactive=False)
253
-
 
 
 
 
254
  def show_best(p=1):
255
  d, t = page(_load_best(), p)
256
  return html(d, p, t), p
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
  def prev(b):
259
  b = max(1, b-1)
@@ -265,29 +228,15 @@ def build():
265
  b = min(maxp, b+1)
266
  h, _ = show_best(b)
267
  return h, b
268
-
269
- # 페이지 네비게이션 버튼 (하단에 배치)
270
- with gr.Row(elem_classes="navigation-buttons"):
271
- b_prev = gr.Button("⬅️ 이전 페이지", size="sm")
272
- b_next = gr.Button("다음 페이지 ➡️", size="sm")
273
-
274
  # 이벤트 연결
 
275
  b_prev.click(prev, inputs=[bp], outputs=[out, bp])
276
  b_next.click(nxt, inputs=[bp], outputs=[out, bp])
277
-
278
- # CSS 스타일 추가
279
- css = """
280
- .navigation-buttons {
281
- display: flex;
282
- justify-content: center;
283
- margin-bottom: 40px;
284
- }
285
- .navigation-buttons button {
286
- margin: 0 10px;
287
- min-width: 120px;
288
- }
289
- """
290
  demo.load(show_best, outputs=[out, bp])
 
291
  return demo
292
 
293
  app = build()
 
1
  import os, re, time, json, datetime, requests, gradio as gr
2
 
3
+ # ───────────────────── 1. 기본 설정 ─────────────────────
4
+ BEST_FILE, PER_PAGE = "best_games.json", 9
 
 
 
 
 
 
 
 
5
 
6
  # ───────────────────── 2. BEST 데이터 ────────────────────
 
7
  def _init_best():
8
  if not os.path.exists(BEST_FILE):
9
  json.dump([], open(BEST_FILE, "w"))
10
+
11
  def _load_best():
12
  try:
13
  data = json.load(open(BEST_FILE))
 
63
  print(f"URL 추가 오류: {str(e)}")
64
  return False
65
 
66
+ # ───────────────────── 4. 페이지네이션 ───────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  def page(lst, pg):
68
+ s = (pg-1) * PER_PAGE
69
+ e = s + PER_PAGE
70
+ total = (len(lst) + PER_PAGE - 1) // PER_PAGE
71
  return lst[s:e], total
72
 
73
+ # ───────────────────── 5. HTML 그리드 ───────────────────
74
  def html(cards, pg, total):
75
  if not cards:
76
  return "<div style='text-align:center;padding:70px;color:#555;'>표시할 배포가 없습니다.</div>"
77
+ css = r"""
78
  <style>
79
+ body {
80
+ margin: 0;
81
+ padding: 0;
82
+ font-family: Poppins, sans-serif;
83
+ background: #f0f0f0;
84
+ }
85
+ .container {
86
+ display: flex;
87
+ flex-direction: column;
88
+ width: 100%;
89
+ box-sizing: border-box;
90
+ }
91
+ .grid {
92
+ display: grid;
93
+ grid-template-columns: repeat(3, 1fr);
94
+ grid-template-rows: repeat(3, 1fr);
95
+ gap: 10px;
96
+ width: 100%;
97
+ height: calc(100vh - 150px);
98
+ box-sizing: border-box;
99
+ padding: 10px;
100
+ }
101
+ .card {
102
+ background: #fff;
103
+ border-radius: 10px;
104
+ overflow: hidden;
105
+ box-shadow: 0 4px 8px rgba(0,0,0,0.1);
106
+ display: flex;
107
+ flex-direction: column;
108
+ height: 100%;
109
+ }
110
+ .hdr {
111
+ padding: 10px;
112
+ background: rgba(255,255,255,.9);
113
+ border-bottom: 1px solid #eee;
114
+ }
115
+ .ttl {
116
+ margin: 0;
117
+ font-size: 1rem;
118
+ font-weight: 600;
119
+ color: #333;
120
+ white-space: nowrap;
121
+ overflow: hidden;
122
+ text-overflow: ellipsis;
123
+ }
124
+ .date {
125
+ margin-top: 2px;
126
+ font-size: 0.75rem;
127
+ color: #777;
128
+ }
129
+ .frame {
130
+ flex: 1;
131
+ position: relative;
132
+ }
133
+ .frame iframe {
134
+ position: absolute;
135
+ top: 0;
136
+ left: 0;
137
+ width: 100%;
138
+ height: 100%;
139
+ border: 0;
140
+ }
141
+ .foot {
142
+ padding: 8px;
143
+ background: rgba(255,255,255,.9);
144
+ text-align: right;
145
+ }
146
+ .link {
147
+ font-size: 0.8rem;
148
+ font-weight: 600;
149
+ color: #4a6dd8;
150
+ text-decoration: none;
151
+ }
152
+ .pagination {
153
+ display: flex;
154
+ justify-content: center;
155
+ align-items: center;
156
+ padding: 10px 0;
157
+ }
158
+ .page-info {
159
+ font-size: 0.85rem;
160
+ color: #555;
161
+ margin: 0 15px;
162
+ }
163
  </style>"""
164
 
165
+ h = css + "<div class='container'><div class='grid'>"
166
  for c in cards:
167
+ date = datetime.datetime.fromtimestamp(int(c["ts"])).strftime("%Y-%m-%d")
168
+ h += f"""
169
  <div class='card'>
170
  <div class='hdr'><p class='ttl'>{c['title']}</p><p class='date'>{date}</p></div>
171
  <div class='frame'><iframe src="{c['url']}" loading="lazy"
172
  allow="accelerometer; camera; encrypted-media; gyroscope;"></iframe></div>
173
  <div class='foot'><a class='link' href="{c['url']}" target="_blank">원본↗</a></div>
174
  </div>"""
175
+ h += "</div></div>"
176
+
177
+ # 페이지 정보만 간단하게 표시
178
+ h += f"<div class='pagination'><span class='page-info'>Page {pg} / {total}</span></div>"
179
+
180
  return h
181
 
182
+ # ───────────────────── 6. Gradio Blocks UI ───────────────────
183
  def build():
184
  _init_best()
185
+ with gr.Blocks(title="Vibe Game Craft", css="body{margin:0;padding:0;overflow:hidden;}") as demo:
186
+ # URL 입력 필드
 
 
187
  with gr.Row():
188
+ title_input = gr.Textbox(label="제목", placeholder="게임 제목")
189
  url_input = gr.Textbox(label="URL", placeholder="https://...")
190
+ add_btn = gr.Button("추가", variant="primary")
191
 
 
 
 
 
 
 
 
 
192
  # 상태 및 출력
193
  bp = gr.State(1)
194
  out = gr.HTML()
195
+
196
+ # 페이지 네비게이션
197
+ with gr.Row():
198
+ b_prev = gr.Button("◀ 이전", size="sm")
199
+ b_next = gr.Button("다음 ▶", size="sm")
200
+
201
  def show_best(p=1):
202
  d, t = page(_load_best(), p)
203
  return html(d, p, t), p
204
+
205
+ def add_url():
206
+ title = title_input.value
207
+ url = url_input.value
208
+
209
+ if not title or not url:
210
+ return
211
+
212
+ if not url.startswith("http"):
213
+ url = "https://" + url
214
+
215
+ add_url_to_best(title, url)
216
+
217
+ # URL 추가 후 화면 갱신
218
+ d, t = page(_load_best(), 1)
219
+ return html(d, 1, t), 1, "", ""
220
 
221
  def prev(b):
222
  b = max(1, b-1)
 
228
  b = min(maxp, b+1)
229
  h, _ = show_best(b)
230
  return h, b
231
+
 
 
 
 
 
232
  # 이벤트 연결
233
+ add_btn.click(add_url, outputs=[out, bp, title_input, url_input])
234
  b_prev.click(prev, inputs=[bp], outputs=[out, bp])
235
  b_next.click(nxt, inputs=[bp], outputs=[out, bp])
236
+
237
+ # 초기 로드
 
 
 
 
 
 
 
 
 
 
 
238
  demo.load(show_best, outputs=[out, bp])
239
+
240
  return demo
241
 
242
  app = build()