seawolf2357 commited on
Commit
fb08ce6
ยท
verified ยท
1 Parent(s): f7e8a67

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +665 -330
app.py CHANGED
@@ -1,366 +1,701 @@
1
- from flask import Flask, render_template, request, redirect, url_for, jsonify, session
2
  import requests
3
  import os
4
- import json
5
  from datetime import timedelta
6
- import logging
7
-
8
- # ๋กœ๊น… ์„ค์ •
9
- logging.basicConfig(level=logging.DEBUG,
10
- format='%(asctime)s - %(levelname)s - %(message)s')
11
- logger = logging.getLogger(__name__)
12
 
13
  app = Flask(__name__)
14
- app.secret_key = os.urandom(24) # ์„ธ์…˜ ์•”ํ˜ธํ™”๋ฅผ ์œ„ํ•œ ๋น„๋ฐ€ ํ‚ค
15
- app.permanent_session_lifetime = timedelta(days=7) # ์„ธ์…˜ ์œ ์ง€ ๊ธฐ๊ฐ„ ์„ค์ •
16
 
17
- # ์ข‹์•„์š” ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ ํ•จ์ˆ˜ - ๋””๋ฒ„๊น… ๊ฐœ์„ 
18
- def get_liked_repos(token):
19
- headers = {"Authorization": f"Bearer {token}"}
20
- liked_models = {}
21
-
22
- # 1. API์— ์ง์ ‘ ์งˆ์˜
23
- endpoints = [
24
- "/api/me/likes",
25
- "/api/me/liked-repos",
26
- "/api/me/favorites"
27
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
- logger.debug(f"ํ† ํฐ์œผ๋กœ ์ข‹์•„์š” ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ ์‹œ๋„ (ํ† ํฐ ์ผ๋ถ€: {token[:4]}...)")
 
 
 
 
 
 
 
 
 
 
 
 
 
30
 
31
- for endpoint in endpoints:
32
- logger.debug(f"์—”๋“œํฌ์ธํŠธ ์‹œ๋„: {endpoint}")
33
-
34
- try:
35
- full_url = f"https://huggingface.co{endpoint}"
36
- logger.debug(f"API ํ˜ธ์ถœ: {full_url}")
37
-
38
- response = requests.get(full_url, headers=headers)
39
- logger.debug(f"์‘๋‹ต ์ƒํƒœ: {response.status_code}")
40
-
41
- if response.ok:
42
- data = response.json()
43
- logger.debug(f"์‘๋‹ต ๋ฐ์ดํ„ฐ ํƒ€์ž…: {type(data)}")
44
- logger.debug(f"๋ฐ์ดํ„ฐ ์ƒ˜ํ”Œ: {str(data)[:200]}")
45
-
46
- if isinstance(data, list):
47
- logger.debug(f"๋ชฉ๋ก ๊ธธ์ด: {len(data)}")
48
-
49
- for i, model in enumerate(data[:5]): # ์ฒ˜์Œ 5๊ฐœ๋งŒ ๋กœ๊น…
50
- logger.debug(f"๋ชจ๋ธ {i}: {model}")
51
-
52
- if isinstance(model, dict):
53
- # ๋‹ค์–‘ํ•œ API ์‘๋‹ต ๊ตฌ์กฐ ์ฒ˜๋ฆฌ
54
- if 'owner' in model and 'name' in model:
55
- model_id = f"{model['owner']}/{model['name']}"
56
- liked_models[model_id] = True
57
- logger.debug(f"์ถ”๊ฐ€๋œ ๋ชจ๋ธ ID: {model_id}")
58
- elif 'id' in model:
59
- liked_models[model['id']] = True
60
- logger.debug(f"์ถ”๊ฐ€๋œ ๋ชจ๋ธ ID: {model['id']}")
61
- elif 'modelId' in model:
62
- liked_models[model['modelId']] = True
63
- logger.debug(f"์ถ”๊ฐ€๋œ ๋ชจ๋ธ ID: {model['modelId']}")
64
- else:
65
- # ๋‹ค๋ฅธ ํ•„๋“œ ํ™•์ธ
66
- logger.debug(f"๋ชจ๋ธ ํ‚ค: {list(model.keys())}")
67
-
68
- # ์ง์ ‘ owner/repo ํ˜•์‹ ์ถ”์ถœ ์‹œ๋„
69
- if 'repo' in model and 'rowner' in model:
70
- model_id = f"{model['rowner']}/{model['repo']}"
71
- liked_models[model_id] = True
72
- logger.debug(f"์ถ”๊ฐ€๋œ ๋ชจ๋ธ ID: {model_id}")
73
- elif isinstance(data, dict):
74
- logger.debug(f"๊ฐ์ฒด ํ‚ค: {list(data.keys())}")
75
-
76
- # ๊ฐ์ฒด ๊ตฌ์กฐ์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌ
77
- if 'models' in data and isinstance(data['models'], list):
78
- for model in data['models']:
79
- if isinstance(model, dict) and 'id' in model:
80
- liked_models[model['id']] = True
81
- logger.debug(f"์ถ”๊ฐ€๋œ ๋ชจ๋ธ ID: {model['id']}")
82
- else:
83
- # ๋‹จ์ˆœ ํ‚ค-๊ฐ’ ๊ตฌ์กฐ๋ผ๋ฉด
84
- for key in data:
85
- liked_models[key] = True
86
- logger.debug(f"์ถ”๊ฐ€๋œ ํ‚ค: {key}")
87
-
88
- logger.debug(f"์ด ์ข‹์•„์š” ํ•ญ๋ชฉ ์ˆ˜: {len(liked_models)}")
89
-
90
- if len(liked_models) > 0:
91
- logger.debug("์ข‹์•„์š” ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ ์„ฑ๊ณต!")
92
- return liked_models
93
- else:
94
- logger.debug(f"API ์˜ค๋ฅ˜: {response.text}")
95
- except Exception as e:
96
- logger.error(f"์—”๋“œํฌ์ธํŠธ {endpoint} ์˜ค๋ฅ˜: {str(e)}")
97
-
98
- # 2. ์ˆ˜๋™ ์ŠคํŽ˜์ด์Šค/๋ชจ๋ธ ํ™•์ธ (API๊ฐ€ ์‹คํŒจํ•œ ๊ฒฝ์šฐ)
99
- logger.debug("API ์กฐํšŒ ์‹คํŒจ, ์ˆ˜๋™ ์ŠคํŽ˜์ด์Šค/๋ชจ๋ธ ํ™•์ธ ์‹œ๋„")
100
 
101
  try:
102
- # "me" ํŽ˜์ด์ง€ ์ ‘๊ทผํ•ด์„œ HTML์—์„œ ์ข‹์•„์š” ์ •๋ณด ์ฐพ๊ธฐ
103
- response = requests.get("https://huggingface.co/me/likes", headers=headers)
104
-
105
  if response.ok:
106
- logger.debug("์ข‹์•„์š” ํŽ˜์ด์ง€ ๊ฐ€์ ธ์˜ค๊ธฐ ์„ฑ๊ณต")
107
- # ์—ฌ๊ธฐ์„œ HTML ํŒŒ์‹ฑํ•  ์ˆ˜๋„ ์žˆ์ง€๋งŒ ๋ณต์žกํ•˜๋ฏ€๋กœ ์ƒ๋žต
108
  except Exception as e:
109
- logger.error(f"์ˆ˜๋™ ํ™•์ธ ์˜ค๋ฅ˜: {str(e)}")
110
 
111
- # 3. API ๊ฒฐ๊ณผ ํ™•์ธ ํŽ˜์ด์ง€ ์ถ”๊ฐ€ (๋””๋ฒ„๊น…์šฉ)
112
- @app.route('/api/debug-likes', methods=['GET'])
113
- def debug_likes():
114
- if 'token' not in session:
115
- return jsonify({'success': False, 'message': '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'})
116
-
117
- headers = {"Authorization": f"Bearer {session['token']}"}
118
- results = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- for endpoint in ["/api/me/likes", "/api/me/liked-repos", "/api/me/favorites"]:
121
- try:
122
- response = requests.get(f"https://huggingface.co{endpoint}", headers=headers)
123
- results[endpoint] = {
124
- 'status': response.status_code,
125
- 'ok': response.ok,
126
- 'data': response.json() if response.ok else None
127
- }
128
- except Exception as e:
129
- results[endpoint] = {'error': str(e)}
130
 
131
- return jsonify(results)
 
 
 
 
132
 
133
- # ์ข‹์•„์š” ์ƒํƒœ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ๋นˆ ๊ฐ์ฒด ๋ฐ˜ํ™˜
134
- logger.debug("์ข‹์•„์š” ๋ชฉ๋ก ์—†์Œ, ๋นˆ ๊ฐ์ฒด ๋ฐ˜ํ™˜")
135
- return liked_models
 
 
 
 
 
136
 
137
- # ๋ฉ”์ธ ์•ฑ์šฉ ํ”Œ๋ž˜๊ทธ๋ฅผ ์ถ”๊ฐ€ํ•ด ๋””๋ฒ„๊น… ์—”๋“œํฌ์ธํŠธ ์ถ”๊ฐ€
138
- def add_debug_endpoints(app):
139
- @app.route('/api/debug-likes', methods=['GET'])
140
- def debug_likes():
141
- if 'token' not in session:
142
- return jsonify({'success': False, 'message': '๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
- headers = {"Authorization": f"Bearer {session['token']}"}
145
- results = {}
 
 
 
 
 
146
 
147
- for endpoint in ["/api/me/likes", "/api/me/liked-repos", "/api/me/favorites"]:
148
- try:
149
- response = requests.get(f"https://huggingface.co{endpoint}", headers=headers)
150
- results[endpoint] = {
151
- 'status': response.status_code,
152
- 'ok': response.ok
153
- }
154
-
155
- # ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ์—๋งŒ ๋ฐ์ดํ„ฐ ์ถ”๊ฐ€
156
- if response.ok:
157
- try:
158
- results[endpoint]['data'] = response.json()
159
- except Exception as e:
160
- results[endpoint]['parse_error'] = str(e)
161
- except Exception as e:
162
- results[endpoint] = {'error': str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
- # ํ˜„์žฌ ์„ธ์…˜์— ์ €์žฅ๋œ ์ข‹์•„์š” ๋ชฉ๋ก ๋ฐ˜ํ™˜
165
- results['current_session_likes'] = {
166
- 'count': len(session.get('liked_models', {})),
167
- 'items': list(session.get('liked_models', {}).keys())
 
 
 
 
 
 
 
168
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
- return jsonify(results)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- @app.route('/api/debug-urls', methods=['GET'])
173
- def debug_urls():
174
- # ๋ชจ๋ธ ์ •๋ณด ์ถ”์ถœ ๋””๋ฒ„๊น…
175
- from urllib.parse import urlparse
 
 
 
 
 
 
176
 
177
- test_urls = [
178
- "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game",
179
- "https://huggingface.co/models/meta/llama-2",
180
- "https://huggingface.co/meta/llama-2"
181
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
183
- results = {}
 
 
184
 
185
- for url in test_urls:
186
- try:
187
- parsed = urlparse(url)
188
- path_parts = parsed.path.split('/')
189
- path_parts = [p for p in path_parts if p] # ๋นˆ ๋ฌธ์ž์—ด ์ œ๊ฑฐ
190
-
191
- result = {
192
- 'parsed_url': {
193
- 'scheme': parsed.scheme,
194
- 'netloc': parsed.netloc,
195
- 'path': parsed.path,
196
- 'path_parts': path_parts
197
- }
198
- }
199
-
200
- # ๊ฒฝ๋กœ ํŒŒ์‹ฑ
201
- if len(path_parts) >= 2:
202
- if path_parts[0] == 'spaces' or path_parts[0] == 'models':
203
- result['extract'] = {
204
- 'type': path_parts[0],
205
- 'owner': path_parts[1],
206
- 'repo': path_parts[2] if len(path_parts) > 2 else None,
207
- 'full_id': f"{path_parts[1]}/{path_parts[2]}" if len(path_parts) > 2 else f"{path_parts[1]}"
208
- }
209
- else:
210
- result['extract'] = {
211
- 'type': 'models', # ๊ธฐ๋ณธ๊ฐ’
212
- 'owner': path_parts[0],
213
- 'repo': path_parts[1] if len(path_parts) > 1 else None,
214
- 'full_id': f"{path_parts[0]}/{path_parts[1]}" if len(path_parts) > 1 else f"{path_parts[0]}"
215
- }
216
-
217
- results[url] = result
218
- except Exception as e:
219
- results[url] = {'error': str(e)}
220
 
221
- return jsonify(results)
222
-
223
- # ์ด ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ง์ ‘ ์‹คํ–‰๋˜๋ฉด ํ…Œ์ŠคํŠธ ์„œ๋ฒ„๋ฅผ ์‹œ์ž‘
224
- if __name__ == '__main__':
225
- test_app = Flask(__name__)
226
- test_app.secret_key = os.urandom(24)
227
-
228
- # ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ •
229
- test_app.config['SESSION_COOKIE_SECURE'] = False # HTTPS ์—†์ด๋„ ์ž‘๋™
230
- test_app.config['SESSION_COOKIE_HTTPONLY'] = True
231
-
232
- add_debug_endpoints(test_app)
233
-
234
- @test_app.route('/')
235
- def home():
236
- return '''
237
- <h1>ํ—ˆ๊น…ํŽ˜์ด์Šค ์ข‹์•„์š” ๋””๋ฒ„๊ฑฐ</h1>
238
- <p>๋‹ค์Œ ์—”๋“œํฌ์ธํŠธ๋กœ ์ ‘์†ํ•˜์„ธ์š”:</p>
239
- <ul>
240
- <li><a href="/login-form">๋กœ๊ทธ์ธ ํŽ˜์ด์ง€</a></li>
241
- <li><a href="/api/debug-likes">์ข‹์•„์š” API ๋””๋ฒ„๊ทธ (๋กœ๊ทธ์ธ ํ•„์š”)</a></li>
242
- <li><a href="/api/debug-urls">URL ํŒŒ์‹ฑ ๋””๋ฒ„๊ทธ</a></li>
243
- </ul>
244
- '''
245
-
246
- @test_app.route('/login-form')
247
- def login_form():
248
- return '''
249
- <!DOCTYPE html>
250
- <html>
251
- <head>
252
- <title>ํ† ํฐ ๋กœ๊ทธ์ธ</title>
253
- <style>
254
- body { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; }
255
- input { width: 100%; padding: 8px; margin: 10px 0; }
256
- button { padding: 10px 15px; background: #4CAF50; color: white; border: none; cursor: pointer; }
257
- .result { margin-top: 20px; padding: 10px; border: 1px solid #ddd; display: none; }
258
- pre { background: #f5f5f5; padding: 10px; overflow-x: auto; }
259
- </style>
260
- </head>
261
- <body>
262
- <h1>ํ—ˆ๊น…ํŽ˜์ด์Šค ํ† ํฐ ํ…Œ์ŠคํŠธ</h1>
263
- <p>ํ—ˆ๊น…ํŽ˜์ด์Šค API ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š”:</p>
264
-
265
- <input type="password" id="tokenInput" placeholder="API ํ† ํฐ" />
266
- <button id="loginBtn">๋กœ๊ทธ์ธ</button>
267
-
268
- <div id="result" class="result">
269
- <h3>๊ฒฐ๊ณผ</h3>
270
- <pre id="resultContent"></pre>
271
- </div>
272
-
273
- <script>
274
- const loginBtn = document.getElementById('loginBtn');
275
- const tokenInput = document.getElementById('tokenInput');
276
- const result = document.getElementById('result');
277
- const resultContent = document.getElementById('resultContent');
278
-
279
- loginBtn.addEventListener('click', async () => {
280
- const token = tokenInput.value.trim();
281
- if (!token) {
282
- alert('ํ† ํฐ์„ ์ž…๋ ฅํ•˜์„ธ์š”');
283
- return;
284
- }
285
-
286
- try {
287
- const formData = new FormData();
288
- formData.append('token', token);
289
-
290
- const response = await fetch('/api/login-test', {
291
- method: 'POST',
292
- body: formData
293
- });
294
-
295
- const data = await response.json();
296
- result.style.display = 'block';
297
- resultContent.textContent = JSON.stringify(data, null, 2);
298
-
299
- if (data.success) {
300
- // ์„ฑ๊ณต ์‹œ ์ข‹์•„์š” ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ์ด๋™
301
- setTimeout(() => {
302
- window.location.href = '/api/debug-likes';
303
- }, 2000);
304
- }
305
- } catch (error) {
306
- result.style.display = 'block';
307
- resultContent.textContent = `์˜ค๋ฅ˜: ${error.message}`;
308
- }
309
- });
310
- </script>
311
- </body>
312
- </html>
313
- '''
314
-
315
- @test_app.route('/api/login-test', methods=['POST'])
316
- def login_test():
317
- token = request.form.get('token', '')
318
 
319
- if not token:
320
- return jsonify({'success': False, 'message': 'ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'})
 
 
 
 
 
 
 
 
 
 
321
 
322
- session['token'] = token
 
323
 
324
- # ์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
325
- user_info = None
326
- for endpoint in ["https://huggingface.co/api/whoami-v2", "https://huggingface.co/api/whoami"]:
327
- try:
328
- headers = {"Authorization": f"Bearer {token}"}
329
- response = requests.get(endpoint, headers=headers)
330
-
331
- if response.ok:
332
- user_info = response.json()
333
- break
334
- except Exception as e:
335
- logger.error(f"์‚ฌ์šฉ์ž ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ ์˜ค๋ฅ˜: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
336
 
337
- if not user_info:
338
- return jsonify({'success': False, 'message': '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.'})
 
339
 
340
- # ์‚ฌ์šฉ์ž ์ด๋ฆ„ ์ฐพ๊ธฐ
341
- username = None
342
- if 'name' in user_info:
343
- username = user_info['name']
344
- elif 'user' in user_info and 'username' in user_info['user']:
345
- username = user_info['user']['username']
346
- elif 'username' in user_info:
347
- username = user_info['username']
348
- else:
349
- username = '์ธ์ฆ๋œ ์‚ฌ์šฉ์ž'
350
 
351
- session['username'] = username
 
 
 
 
 
352
 
353
- # ์ข‹์•„์š” ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
354
- liked_models = get_liked_repos(token)
355
- session['liked_models'] = liked_models
 
356
 
357
- return jsonify({
358
- 'success': True,
359
- 'username': username,
360
- 'liked_models_count': len(liked_models),
361
- 'liked_models_sample': list(liked_models.keys())[:5] if liked_models else []
362
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
- # ๋””๋ฒ„๊น… ์„œ๋ฒ„ ์‹คํ–‰
365
- logger.info("๋””๋ฒ„๊น… ์„œ๋ฒ„ ์‹œ์ž‘ (ํฌํŠธ 7860)")
366
- test_app.run(host='0.0.0.0', port=7860, debug=True)
 
1
+ from flask import Flask, render_template, request, jsonify, session
2
  import requests
3
  import os
 
4
  from datetime import timedelta
 
 
 
 
 
 
5
 
6
  app = Flask(__name__)
7
+ app.secret_key = os.urandom(24)
8
+ app.permanent_session_lifetime = timedelta(days=7)
9
 
10
+ # Hugging Face URL ๋ชฉ๋ก
11
+ HUGGINGFACE_URLS = [
12
+ "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game",
13
+ "https://huggingface.co/spaces/openfree/deepseek_r1_API",
14
+ "https://huggingface.co/spaces/ginipick/open_Deep-Research",
15
+ "https://huggingface.co/spaces/aiqmaster/open-deep-research",
16
+ "https://huggingface.co/spaces/seawolf2357/DeepSeek-R1-32b-search",
17
+ "https://huggingface.co/spaces/ginigen/LLaDA",
18
+ "https://huggingface.co/spaces/VIDraft/PHI4-Multimodal",
19
+ "https://huggingface.co/spaces/ginigen/Ovis2-8B",
20
+ "https://huggingface.co/spaces/ginigen/Graph-Mind",
21
+ "https://huggingface.co/spaces/ginigen/Workflow-Canvas",
22
+ "https://huggingface.co/spaces/ginigen/Design",
23
+ "https://huggingface.co/spaces/ginigen/Diagram",
24
+ "https://huggingface.co/spaces/ginigen/Mockup",
25
+ "https://huggingface.co/spaces/ginigen/Infographic",
26
+ "https://huggingface.co/spaces/ginigen/Flowchart",
27
+ "https://huggingface.co/spaces/aiqcamp/FLUX-Vision",
28
+ "https://huggingface.co/spaces/ginigen/VoiceClone-TTS",
29
+ "https://huggingface.co/spaces/openfree/Perceptron-Network",
30
+ "https://huggingface.co/spaces/openfree/Article-Generator",
31
+ ]
32
+
33
+ # URL์—์„œ ๋ชจ๋ธ/์ŠคํŽ˜์ด์Šค ์ •๋ณด ์ถ”์ถœ
34
+ def extract_model_info(url):
35
+ parts = url.split('/')
36
+ if len(parts) < 6:
37
+ return None
38
 
39
+ if parts[3] == 'spaces' or parts[3] == 'models':
40
+ return {
41
+ 'type': parts[3],
42
+ 'owner': parts[4],
43
+ 'repo': parts[5],
44
+ 'full_id': f"{parts[4]}/{parts[5]}"
45
+ }
46
+ elif len(parts) >= 5:
47
+ return {
48
+ 'type': 'models',
49
+ 'owner': parts[3],
50
+ 'repo': parts[4],
51
+ 'full_id': f"{parts[3]}/{parts[4]}"
52
+ }
53
 
54
+ return None
55
+
56
+ # URL์˜ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„์„ ์ œ๋ชฉ์œผ๋กœ ์ถ”์ถœ
57
+ def extract_title(url):
58
+ parts = url.split("/")
59
+ title = parts[-1] if parts else ""
60
+ return title.replace("_", " ").replace("-", " ")
61
+
62
+ # ํ—ˆ๊น…ํŽ˜์ด์Šค ์‚ฌ์šฉ์ž ์ธ์ฆ
63
+ def validate_token(token):
64
+ headers = {"Authorization": f"Bearer {token}"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
  try:
67
+ response = requests.get("https://huggingface.co/api/whoami-v2", headers=headers)
 
 
68
  if response.ok:
69
+ return True, response.json()
 
70
  except Exception as e:
71
+ print(f"ํ† ํฐ ๊ฒ€์ฆ ์˜ค๋ฅ˜: {e}")
72
 
73
+ return False, None
74
+
75
+ @app.route('/')
76
+ def home():
77
+ return render_template('index.html')
78
+
79
+ @app.route('/api/login', methods=['POST'])
80
+ def login():
81
+ token = request.form.get('token', '')
82
+
83
+ if not token:
84
+ return jsonify({'success': False, 'message': 'ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.'})
85
+
86
+ is_valid, user_info = validate_token(token)
87
+
88
+ if not is_valid or not user_info:
89
+ return jsonify({'success': False, 'message': '์œ ํšจํ•˜์ง€ ์•Š์€ ํ† ํฐ์ž…๋‹ˆ๋‹ค.'})
90
+
91
+ # ์‚ฌ์šฉ์ž ์ด๋ฆ„ ์ฐพ๊ธฐ
92
+ username = None
93
+ if 'name' in user_info:
94
+ username = user_info['name']
95
+ elif 'user' in user_info and 'username' in user_info['user']:
96
+ username = user_info['user']['username']
97
+ elif 'username' in user_info:
98
+ username = user_info['username']
99
+ else:
100
+ username = '์ธ์ฆ๋œ ์‚ฌ์šฉ์ž'
101
+
102
+ # ์„ธ์…˜์— ์ €์žฅ
103
+ session['token'] = token
104
+ session['username'] = username
105
+
106
+ return jsonify({
107
+ 'success': True,
108
+ 'username': username
109
+ })
110
+
111
+ @app.route('/api/logout', methods=['POST'])
112
+ def logout():
113
+ session.pop('token', None)
114
+ session.pop('username', None)
115
+ return jsonify({'success': True})
116
+
117
+ @app.route('/api/urls', methods=['GET'])
118
+ def get_urls():
119
+ results = []
120
+ for url in HUGGINGFACE_URLS:
121
+ title = extract_title(url)
122
+ model_info = extract_model_info(url)
123
 
124
+ if not model_info:
125
+ continue
 
 
 
 
 
 
 
 
126
 
127
+ results.append({
128
+ 'url': url,
129
+ 'title': title,
130
+ 'model_info': model_info
131
+ })
132
 
133
+ return jsonify(results)
134
+
135
+ @app.route('/api/session-status', methods=['GET'])
136
+ def session_status():
137
+ return jsonify({
138
+ 'logged_in': 'token' in session,
139
+ 'username': session.get('username')
140
+ })
141
 
142
+ if __name__ == '__main__':
143
+ os.makedirs('templates', exist_ok=True)
144
+
145
+ with open('templates/index.html', 'w', encoding='utf-8') as f:
146
+ f.write('''
147
+ <!DOCTYPE html>
148
+ <html lang="ko">
149
+ <head>
150
+ <meta charset="UTF-8">
151
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
152
+ <title>Hugging Face URL ์นด๋“œ ๋ฆฌ์ŠคํŠธ</title>
153
+ <style>
154
+ body {
155
+ font-family: Arial, sans-serif;
156
+ line-height: 1.6;
157
+ margin: 0;
158
+ padding: 0;
159
+ color: #333;
160
+ }
161
+
162
+ .container {
163
+ max-width: 1200px;
164
+ margin: 0 auto;
165
+ padding: 1rem;
166
+ }
167
+
168
+ .header {
169
+ background-color: #f0f0f0;
170
+ padding: 1rem;
171
+ border-radius: 5px;
172
+ margin-bottom: 1rem;
173
+ }
174
+
175
+ .user-controls {
176
+ display: flex;
177
+ justify-content: space-between;
178
+ align-items: center;
179
+ flex-wrap: wrap;
180
+ }
181
+
182
+ .filter-controls {
183
+ background-color: #f8f9fa;
184
+ padding: 1rem;
185
+ border-radius: 5px;
186
+ margin-bottom: 1rem;
187
+ }
188
+
189
+ input[type="password"],
190
+ input[type="text"] {
191
+ padding: 0.5rem;
192
+ border: 1px solid #ccc;
193
+ border-radius: 4px;
194
+ margin-right: 5px;
195
+ }
196
+
197
+ button {
198
+ padding: 0.5rem 1rem;
199
+ background-color: #4CAF50;
200
+ color: white;
201
+ border: none;
202
+ border-radius: 4px;
203
+ cursor: pointer;
204
+ }
205
+
206
+ button:hover {
207
+ background-color: #45a049;
208
+ }
209
+
210
+ button.logout {
211
+ background-color: #f44336;
212
+ }
213
+
214
+ button.logout:hover {
215
+ background-color: #d32f2f;
216
+ }
217
+
218
+ .token-help {
219
+ margin-top: 0.5rem;
220
+ font-size: 0.8rem;
221
+ color: #666;
222
+ }
223
+
224
+ .token-help a {
225
+ color: #4CAF50;
226
+ text-decoration: none;
227
+ }
228
+
229
+ .token-help a:hover {
230
+ text-decoration: underline;
231
+ }
232
+
233
+ .cards-container {
234
+ display: flex;
235
+ flex-wrap: wrap;
236
+ gap: 1rem;
237
+ }
238
+
239
+ .card {
240
+ border: 1px solid #ccc;
241
+ border-radius: 5px;
242
+ padding: 1rem;
243
+ width: 300px;
244
+ box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
245
+ position: relative;
246
+ background-color: #f9f9f9;
247
+ }
248
+
249
+ .card a {
250
+ text-decoration: none;
251
+ color: #333;
252
+ word-break: break-all;
253
+ }
254
+
255
+ .like-button {
256
+ position: absolute;
257
+ top: 1rem;
258
+ right: 1rem;
259
+ border: none;
260
+ background: transparent;
261
+ font-size: 1.5rem;
262
+ cursor: pointer;
263
+ transition: color 0.2s;
264
+ }
265
+
266
+ .like-button.liked {
267
+ color: red;
268
+ }
269
+
270
+ .like-button.not-liked {
271
+ color: white;
272
+ -webkit-text-stroke: 1px #333;
273
+ }
274
+
275
+ .status-message {
276
+ padding: 1rem;
277
+ border-radius: 4px;
278
+ margin-bottom: 1rem;
279
+ display: none;
280
+ }
281
+
282
+ .success {
283
+ background-color: #dff0d8;
284
+ color: #3c763d;
285
+ }
286
+
287
+ .error {
288
+ background-color: #f2dede;
289
+ color: #a94442;
290
+ }
291
+
292
+ .loading {
293
+ position: fixed;
294
+ top: 0;
295
+ left: 0;
296
+ right: 0;
297
+ bottom: 0;
298
+ background-color: rgba(255, 255, 255, 0.7);
299
+ display: none;
300
+ justify-content: center;
301
+ align-items: center;
302
+ z-index: 1000;
303
+ font-size: 1.5rem;
304
+ }
305
+
306
+ .login-section {
307
+ margin-top: 1rem;
308
+ }
309
+
310
+ .logged-in-section {
311
+ display: none;
312
+ margin-top: 1rem;
313
+ }
314
+
315
+ @media (max-width: 768px) {
316
+ .user-controls {
317
+ flex-direction: column;
318
+ align-items: flex-start;
319
+ }
320
+
321
+ .user-controls > div {
322
+ margin-bottom: 1rem;
323
+ }
324
+
325
+ .card {
326
+ width: 100%;
327
+ }
328
+ }
329
+ </style>
330
+ </head>
331
+ <body>
332
+ <div class="container">
333
+ <div class="header">
334
+ <div class="user-controls">
335
+ <div>
336
+ <span>ํ—ˆ๊น…ํŽ˜์ด์Šค ๊ณ„์ •: </span>
337
+ <span id="currentUser">๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์Œ</span>
338
+ </div>
339
 
340
+ <div id="loginSection" class="login-section">
341
+ <input type="password" id="tokenInput" placeholder="ํ—ˆ๊น…ํŽ˜์ด์Šค API ํ† ํฐ ์ž…๋ ฅ" />
342
+ <button id="loginButton">์ธ์ฆํ•˜๊ธฐ</button>
343
+ <div class="token-help">
344
+ API ํ† ํฐ์€ <a href="https://huggingface.co/settings/tokens" target="_blank">ํ—ˆ๊น…ํŽ˜์ด์Šค ํ† ํฐ ํŽ˜์ด์ง€</a>์—์„œ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
345
+ </div>
346
+ </div>
347
 
348
+ <div id="loggedInSection" class="logged-in-section">
349
+ <button id="logoutButton" class="logout">๋กœ๊ทธ์•„์›ƒ</button>
350
+ </div>
351
+ </div>
352
+ </div>
353
+
354
+ <div class="filter-controls">
355
+ <label>
356
+ <input type="checkbox" id="showOnlyLiked" />
357
+ ๋‚ด๊ฐ€ ์ข‹์•„์š”ํ•œ URL๋งŒ ๋ณด๊ธฐ
358
+ </label>
359
+ <label style="margin-left: 1rem;">
360
+ <input type="text" id="searchInput" placeholder="URL ๋˜๋Š” ์ œ๋ชฉ์œผ๋กœ ๊ฒ€์ƒ‰" style="width: 250px;" />
361
+ </label>
362
+ </div>
363
+
364
+ <div id="statusMessage" class="status-message"></div>
365
+
366
+ <div id="loadingIndicator" class="loading">์ฒ˜๋ฆฌ ์ค‘...</div>
367
+
368
+ <div id="cardsContainer" class="cards-container"></div>
369
+ </div>
370
+
371
+ <script>
372
+ // DOM ์š”์†Œ ์ฐธ์กฐ
373
+ const elements = {
374
+ tokenInput: document.getElementById('tokenInput'),
375
+ loginButton: document.getElementById('loginButton'),
376
+ logoutButton: document.getElementById('logoutButton'),
377
+ currentUser: document.getElementById('currentUser'),
378
+ cardsContainer: document.getElementById('cardsContainer'),
379
+ loadingIndicator: document.getElementById('loadingIndicator'),
380
+ statusMessage: document.getElementById('statusMessage'),
381
+ showOnlyLiked: document.getElementById('showOnlyLiked'),
382
+ searchInput: document.getElementById('searchInput'),
383
+ loginSection: document.getElementById('loginSection'),
384
+ loggedInSection: document.getElementById('loggedInSection')
385
+ };
386
+
387
+ // ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ
388
+ const state = {
389
+ username: null,
390
+ likedURLs: {},
391
+ isLoading: false
392
+ };
393
+
394
+ // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€ ํ‚ค
395
+ function getLikesStorageKey(username) {
396
+ return `hf_local_likes_${username}`;
397
+ }
398
+
399
+ // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ข‹์•„์š” ์ •๋ณด ๋กœ๋“œ
400
+ function loadLikesFromStorage() {
401
+ if (!state.username) return {};
402
+
403
+ const key = getLikesStorageKey(state.username);
404
+ const savedLikes = localStorage.getItem(key);
405
+ return savedLikes ? JSON.parse(savedLikes) : {};
406
+ }
407
+
408
+ // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ข‹์•„์š” ์ •๋ณด ์ €์žฅ
409
+ function saveLikesToStorage() {
410
+ if (!state.username) return;
411
+
412
+ const key = getLikesStorageKey(state.username);
413
+ localStorage.setItem(key, JSON.stringify(state.likedURLs));
414
+ }
415
+
416
+ // ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ ํ•จ์ˆ˜
417
+ function setLoading(isLoading) {
418
+ state.isLoading = isLoading;
419
+ elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none';
420
+ }
421
+
422
+ // ์ƒํƒœ ๋ฉ”์‹œ์ง€ ํ‘œ์‹œ ํ•จ์ˆ˜
423
+ function showMessage(message, isError = false) {
424
+ elements.statusMessage.textContent = message;
425
+ elements.statusMessage.className = `status-message ${isError ? 'error' : 'success'}`;
426
+ elements.statusMessage.style.display = 'block';
427
+
428
+ // 3์ดˆ ํ›„ ๋ฉ”์‹œ์ง€ ์‚ฌ๋ผ์ง
429
+ setTimeout(() => {
430
+ elements.statusMessage.style.display = 'none';
431
+ }, 3000);
432
+ }
433
+
434
+ // API ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜
435
+ async function handleApiResponse(response) {
436
+ if (!response.ok) {
437
+ const errorText = await response.text();
438
+ throw new Error(`API ์˜ค๋ฅ˜ (${response.status}): ${errorText}`);
439
+ }
440
+ return response.json();
441
+ }
442
+
443
+ // ์„ธ์…˜ ์ƒํƒœ ํ™•์ธ
444
+ async function checkSessionStatus() {
445
+ try {
446
+ const response = await fetch('/api/session-status');
447
+ const data = await handleApiResponse(response);
448
 
449
+ if (data.logged_in) {
450
+ state.username = data.username;
451
+ elements.currentUser.textContent = data.username;
452
+ elements.loginSection.style.display = 'none';
453
+ elements.loggedInSection.style.display = 'block';
454
+
455
+ // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ข‹์•„์š” ์ •๋ณด ๋กœ๋“œ
456
+ state.likedURLs = loadLikesFromStorage();
457
+
458
+ // URL ๋ชฉ๋ก ๋กœ๋“œ
459
+ loadUrls();
460
  }
461
+ } catch (error) {
462
+ console.error('์„ธ๏ฟฝ๏ฟฝ ์ƒํƒœ ํ™•์ธ ์˜ค๋ฅ˜:', error);
463
+ }
464
+ }
465
+
466
+ // ๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ
467
+ async function login(token) {
468
+ if (!token.trim()) {
469
+ showMessage('ํ† ํฐ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.', true);
470
+ return;
471
+ }
472
+
473
+ setLoading(true);
474
+
475
+ try {
476
+ const formData = new FormData();
477
+ formData.append('token', token);
478
+
479
+ const response = await fetch('/api/login', {
480
+ method: 'POST',
481
+ body: formData
482
+ });
483
+
484
+ const data = await handleApiResponse(response);
485
 
486
+ if (data.success) {
487
+ state.username = data.username;
488
+
489
+ // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์—์„œ ์ข‹์•„์š” ์ •๋ณด ๋กœ๋“œ
490
+ state.likedURLs = loadLikesFromStorage();
491
+
492
+ elements.currentUser.textContent = state.username;
493
+ elements.loginSection.style.display = 'none';
494
+ elements.loggedInSection.style.display = 'block';
495
+
496
+ showMessage(`${state.username}๋‹˜์œผ๋กœ ๋กœ๊ทธ์ธ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`);
497
+
498
+ // URL ๋ชฉ๋ก ๋กœ๋“œ
499
+ loadUrls();
500
+ } else {
501
+ showMessage(data.message || '๋กœ๊ทธ์ธ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', true);
502
+ }
503
+ } catch (error) {
504
+ console.error('๋กœ๊ทธ์ธ ์˜ค๋ฅ˜:', error);
505
+ showMessage(`๋กœ๊ทธ์ธ ์˜ค๋ฅ˜: ${error.message}`, true);
506
+ } finally {
507
+ setLoading(false);
508
+ }
509
+ }
510
 
511
+ // ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ
512
+ async function logout() {
513
+ setLoading(true);
514
+
515
+ try {
516
+ const response = await fetch('/api/logout', {
517
+ method: 'POST'
518
+ });
519
+
520
+ const data = await handleApiResponse(response);
521
 
522
+ if (data.success) {
523
+ state.username = null;
524
+ state.likedURLs = {};
525
+
526
+ elements.currentUser.textContent = '๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์Œ';
527
+ elements.tokenInput.value = '';
528
+ elements.loginSection.style.display = 'block';
529
+ elements.loggedInSection.style.display = 'none';
530
+
531
+ showMessage('๋กœ๊ทธ์•„์›ƒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
532
+
533
+ // ์นด๋“œ ์ดˆ๊ธฐํ™”
534
+ elements.cardsContainer.innerHTML = '';
535
+ }
536
+ } catch (error) {
537
+ console.error('๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜:', error);
538
+ showMessage(`๋กœ๊ทธ์•„์›ƒ ์˜ค๋ฅ˜: ${error.message}`, true);
539
+ } finally {
540
+ setLoading(false);
541
+ }
542
+ }
543
+
544
+ // URL ๋ชฉ๋ก ๋กœ๋“œ
545
+ async function loadUrls() {
546
+ setLoading(true);
547
+
548
+ try {
549
+ const response = await fetch('/api/urls');
550
+ const urls = await handleApiResponse(response);
551
 
552
+ // ํ•„ํ„ฐ๋ง ์ ์šฉ
553
+ const showOnlyLiked = elements.showOnlyLiked.checked;
554
+ const searchText = elements.searchInput.value.toLowerCase();
555
 
556
+ const filteredUrls = urls.filter(item => {
557
+ const { url, title } = item;
558
+
559
+ // ์ข‹์•„์š” ํ•„ํ„ฐ๋ง
560
+ if (showOnlyLiked && !state.likedURLs[url]) {
561
+ return false;
562
+ }
563
+
564
+ // ๊ฒ€์ƒ‰ ํ•„ํ„ฐ๋ง
565
+ if (searchText && !url.toLowerCase().includes(searchText) && !title.toLowerCase().includes(searchText)) {
566
+ return false;
567
+ }
568
+
569
+ return true;
570
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
+ renderCards(filteredUrls);
573
+ } catch (error) {
574
+ console.error('URL ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
575
+ showMessage(`URL ๋กœ๋“œ ์˜ค๋ฅ˜: ${error.message}`, true);
576
+ } finally {
577
+ setLoading(false);
578
+ }
579
+ }
580
+
581
+ // ์ข‹์•„์š” ํ† ๊ธ€
582
+ function toggleLike(url, button) {
583
+ if (!state.username) {
584
+ showMessage('์ข‹์•„์š”๋ฅผ ํ•˜๋ ค๋ฉด ํ—ˆ๊น…ํŽ˜์ด์Šค API ํ† ํฐ์œผ๋กœ ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.', true);
585
+ return;
586
+ }
587
+
588
+ setLoading(true);
589
+
590
+ try {
591
+ // ํ˜„์žฌ ์ข‹์•„์š” ์ƒํƒœ ํ™•์ธ
592
+ const isCurrentlyLiked = state.likedURLs[url] || false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
 
594
+ // ์ƒํƒœ ํ† ๊ธ€
595
+ if (isCurrentlyLiked) {
596
+ delete state.likedURLs[url];
597
+ button.classList.remove('liked');
598
+ button.classList.add('not-liked');
599
+ showMessage('์ข‹์•„์š”๋ฅผ ์ทจ์†Œํ–ˆ์Šต๋‹ˆ๋‹ค.');
600
+ } else {
601
+ state.likedURLs[url] = true;
602
+ button.classList.add('liked');
603
+ button.classList.remove('not-liked');
604
+ showMessage('์ข‹์•„์š”๋ฅผ ์ถ”๊ฐ€ํ–ˆ์Šต๋‹ˆ๋‹ค.');
605
+ }
606
 
607
+ // ๋กœ์ปฌ ์Šคํ† ๋ฆฌ์ง€์— ์ €์žฅ
608
+ saveLikesToStorage();
609
 
610
+ // ํ•„ํ„ฐ๋ง๋œ ๊ฒฝ์šฐ ๋ชฉ๋ก ๋‹ค์‹œ ๋กœ๋“œ
611
+ if (elements.showOnlyLiked.checked) {
612
+ loadUrls();
613
+ }
614
+ } catch (error) {
615
+ console.error('์ข‹์•„์š” ํ† ๊ธ€ ์˜ค๋ฅ˜:', error);
616
+ showMessage(`์ข‹์•„์š” ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: ${error.message}`, true);
617
+ } finally {
618
+ setLoading(false);
619
+ }
620
+ }
621
+
622
+ // ์นด๋“œ ๋ Œ๋”๋ง
623
+ function renderCards(urls) {
624
+ elements.cardsContainer.innerHTML = '';
625
+
626
+ if (!urls || urls.length === 0) {
627
+ const noResultsMsg = document.createElement('p');
628
+ noResultsMsg.textContent = 'ํ‘œ์‹œํ•  URL์ด ์—†์Šต๋‹ˆ๋‹ค.';
629
+ noResultsMsg.style.padding = '1rem';
630
+ noResultsMsg.style.fontStyle = 'italic';
631
+ elements.cardsContainer.appendChild(noResultsMsg);
632
+ return;
633
+ }
634
+
635
+ urls.forEach(item => {
636
+ const { url, title } = item;
637
+ const isLiked = state.likedURLs[url] || false;
638
 
639
+ // ์นด๋“œ ์ƒ์„ฑ
640
+ const card = document.createElement('div');
641
+ card.className = 'card';
642
 
643
+ // ์ œ๋ชฉ
644
+ const titleEl = document.createElement('h3');
645
+ titleEl.textContent = title;
646
+ card.appendChild(titleEl);
 
 
 
 
 
 
647
 
648
+ // URL ๋งํฌ
649
+ const linkEl = document.createElement('a');
650
+ linkEl.href = url;
651
+ linkEl.textContent = url;
652
+ linkEl.target = '_blank';
653
+ card.appendChild(linkEl);
654
 
655
+ // ์ข‹์•„์š” ๋ฒ„ํŠผ (โ™ฅ ์•„์ด์ฝ˜)
656
+ const likeBtn = document.createElement('button');
657
+ likeBtn.className = `like-button ${isLiked ? 'liked' : 'not-liked'}`;
658
+ likeBtn.textContent = 'โ™ฅ';
659
 
660
+ likeBtn.addEventListener('click', function(e) {
661
+ e.preventDefault();
662
+ toggleLike(url, likeBtn);
663
+ });
664
+ card.appendChild(likeBtn);
665
+
666
+ // ์นด๋“œ ์ถ”๊ฐ€
667
+ elements.cardsContainer.appendChild(card);
668
+ });
669
+ }
670
+
671
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์„ค์ •
672
+ elements.loginButton.addEventListener('click', () => {
673
+ login(elements.tokenInput.value);
674
+ });
675
+
676
+ elements.logoutButton.addEventListener('click', logout);
677
+
678
+ // ์—”ํ„ฐ ํ‚ค๋กœ ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅํ•˜๊ฒŒ
679
+ elements.tokenInput.addEventListener('keypress', (event) => {
680
+ if (event.key === 'Enter') {
681
+ login(elements.tokenInput.value);
682
+ }
683
+ });
684
+
685
+ // ํ•„ํ„ฐ๋ง ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
686
+ elements.showOnlyLiked.addEventListener('change', loadUrls);
687
+ elements.searchInput.addEventListener('input', () => {
688
+ // ์ž…๋ ฅ ์ง€์—ฐ ์ฒ˜๋ฆฌ (ํƒ€์ดํ•‘ํ•  ๋•Œ๋งˆ๋‹ค API ํ˜ธ์ถœ ๋ฐฉ์ง€)
689
+ clearTimeout(state.searchTimeout);
690
+ state.searchTimeout = setTimeout(loadUrls, 300);
691
+ });
692
+
693
+ // ์ดˆ๊ธฐํ™”
694
+ checkSessionStatus();
695
+ </script>
696
+ </body>
697
+ </html>
698
+ ''')
699
 
700
+ # ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค์—์„œ๋Š” 7860 ํฌํŠธ ์‚ฌ์šฉ
701
+ app.run(host='0.0.0.0', port=7860)