Spaces:
Running
Running
| from flask import Flask, render_template, request, jsonify, session | |
| import requests | |
| from bs4 import BeautifulSoup | |
| import os | |
| from datetime import timedelta | |
| import logging | |
| import time | |
| # λ‘κΉ μ€μ | |
| logging.basicConfig(level=logging.INFO, | |
| format='%(asctime)s - %(levelname)s - %(message)s') | |
| logger = logging.getLogger(__name__) | |
| app = Flask(__name__) | |
| app.secret_key = os.urandom(24) | |
| app.permanent_session_lifetime = timedelta(days=7) | |
| # Hugging Face URL λͺ©λ‘ | |
| HUGGINGFACE_URLS = [ | |
| "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game", | |
| "https://huggingface.co/spaces/openfree/deepseek_r1_API", | |
| "https://huggingface.co/spaces/ginipick/open_Deep-Research", | |
| "https://huggingface.co/spaces/aiqmaster/open-deep-research", | |
| "https://huggingface.co/spaces/seawolf2357/DeepSeek-R1-32b-search", | |
| "https://huggingface.co/spaces/ginigen/LLaDA", | |
| "https://huggingface.co/spaces/VIDraft/PHI4-Multimodal", | |
| "https://huggingface.co/spaces/ginigen/Ovis2-8B", | |
| "https://huggingface.co/spaces/ginigen/Graph-Mind", | |
| "https://huggingface.co/spaces/ginigen/Workflow-Canvas", | |
| "https://huggingface.co/spaces/ginigen/Design", | |
| "https://huggingface.co/spaces/ginigen/Diagram", | |
| "https://huggingface.co/spaces/ginigen/Mockup", | |
| "https://huggingface.co/spaces/ginigen/Infographic", | |
| "https://huggingface.co/spaces/ginigen/Flowchart", | |
| "https://huggingface.co/spaces/aiqcamp/FLUX-Vision", | |
| "https://huggingface.co/spaces/ginigen/VoiceClone-TTS", | |
| "https://huggingface.co/spaces/openfree/Perceptron-Network", | |
| "https://huggingface.co/spaces/openfree/Article-Generator", | |
| ] | |
| # URLμμ λͺ¨λΈ/μ€νμ΄μ€ μ 보 μΆμΆ | |
| def extract_model_info(url): | |
| parts = url.split('/') | |
| if len(parts) < 6: | |
| return None | |
| if parts[3] == 'spaces' or parts[3] == 'models': | |
| return { | |
| 'type': parts[3], | |
| 'owner': parts[4], | |
| 'repo': parts[5], | |
| 'full_id': f"{parts[4]}/{parts[5]}" | |
| } | |
| elif len(parts) >= 5: | |
| return { | |
| 'type': 'models', | |
| 'owner': parts[3], | |
| 'repo': parts[4], | |
| 'full_id': f"{parts[3]}/{parts[4]}" | |
| } | |
| return None | |
| # URLμ λ§μ§λ§ λΆλΆμ μ λͺ©μΌλ‘ μΆμΆ | |
| def extract_title(url): | |
| parts = url.split("/") | |
| title = parts[-1] if parts else "" | |
| return title.replace("_", " ").replace("-", " ") | |
| # νκΉ νμ΄μ€ ν ν° κ²μ¦ | |
| def validate_token(token): | |
| headers = {"Authorization": f"Bearer {token}"} | |
| try: | |
| response = requests.get("https://huggingface.co/api/whoami-v2", headers=headers) | |
| if response.ok: | |
| return True, response.json() | |
| except Exception as e: | |
| logger.error(f"ν ν° κ²μ¦ μ€λ₯: {e}") | |
| return False, None | |
| # μΉ μ€ν¬λνμΌλ‘ μ’μμ μν νμΈ | |
| def check_like_status_by_scraping(url, token): | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" | |
| } | |
| try: | |
| # νμ΄μ§ μμ² | |
| response = requests.get(url, headers=headers) | |
| if not response.ok: | |
| logger.warning(f"νμ΄μ§ μμ² μ€ν¨: {url}, μν μ½λ: {response.status_code}") | |
| return False | |
| # HTML νμ± | |
| soup = BeautifulSoup(response.text, 'html.parser') | |
| # μ’μμ λ²νΌ μ°ΎκΈ° (λ€μν μ νμ μλ) | |
| like_button = None | |
| selectors = [ | |
| '.like-button-container button', | |
| 'button[aria-label="Like"]', | |
| 'button.like-button', | |
| 'button[data-testid="like-button"]', | |
| 'button.heart-button' | |
| ] | |
| for selector in selectors: | |
| like_button = soup.select_one(selector) | |
| if like_button: | |
| break | |
| if not like_button: | |
| logger.warning(f"μ’μμ λ²νΌμ μ°Ύμ μ μμ: {url}") | |
| return False | |
| # μ’μμ μν νμΈ (ν΄λμ€, aria-pressed μμ± λ±μΌλ‘ νμΈ) | |
| is_liked = False | |
| # ν΄λμ€λ‘ νμΈ | |
| if 'liked' in like_button.get('class', []) or 'active' in like_button.get('class', []): | |
| is_liked = True | |
| # aria-pressed μμ±μΌλ‘ νμΈ | |
| elif like_button.get('aria-pressed') == 'true': | |
| is_liked = True | |
| # λ΄λΆ ν μ€νΈ λλ μμ΄μ½μΌλ‘ νμΈ | |
| elif like_button.find('span', class_='liked') or like_button.find('svg', class_='liked'): | |
| is_liked = True | |
| logger.info(f"μ€ν¬λν κ²°κ³Ό: {url} - μ’μμ {is_liked}") | |
| return is_liked | |
| except Exception as e: | |
| logger.error(f"μ€ν¬λν μ€λ₯ ({url}): {e}") | |
| return False | |
| # μ 체 URL λͺ©λ‘μ μ’μμ μν μ€ν¬λν | |
| def scrape_all_like_status(token): | |
| like_status = {} | |
| for url in HUGGINGFACE_URLS: | |
| try: | |
| # κ³Όλν μμ² λ°©μ§λ₯Ό μν μ§μ° | |
| time.sleep(1) | |
| is_liked = check_like_status_by_scraping(url, token) | |
| like_status[url] = is_liked | |
| logger.info(f"μ’μμ μν νμΈ: {url} - {is_liked}") | |
| except Exception as e: | |
| logger.error(f"URL μ²λ¦¬ μ€ μ€λ₯: {url} - {e}") | |
| like_status[url] = False | |
| return like_status | |
| def home(): | |
| return render_template('index.html') | |
| def login(): | |
| token = request.form.get('token', '') | |
| if not token: | |
| return jsonify({'success': False, 'message': 'ν ν°μ μ λ ₯ν΄μ£ΌμΈμ.'}) | |
| is_valid, user_info = validate_token(token) | |
| if not is_valid or not user_info: | |
| return jsonify({'success': False, 'message': 'μ ν¨νμ§ μμ ν ν°μ λλ€.'}) | |
| # μ¬μ©μ μ΄λ¦ μ°ΎκΈ° | |
| username = None | |
| if 'name' in user_info: | |
| username = user_info['name'] | |
| elif 'user' in user_info and 'username' in user_info['user']: | |
| username = user_info['user']['username'] | |
| elif 'username' in user_info: | |
| username = user_info['username'] | |
| else: | |
| username = 'μΈμ¦λ μ¬μ©μ' | |
| # μΈμ μ μ μ₯ | |
| session['token'] = token | |
| session['username'] = username | |
| # μΉ μ€ν¬λνμΌλ‘ μ’μμ μν νμΈ | |
| # μ°Έκ³ : μ΄ μμ μ΄ μκ°μ΄ μ€λ 걸릴 μ μμΌλ―λ‘ λΉλκΈ°λ‘ μ²λ¦¬νλ κ²μ΄ μ’μ΅λλ€ | |
| # νμ¬λ μμλ‘ λκΈ° λ°©μμΌλ‘ ꡬννμ΅λλ€ | |
| try: | |
| like_status = scrape_all_like_status(token) | |
| session['like_status'] = like_status | |
| except Exception as e: | |
| logger.error(f"μ’μμ μν μ€ν¬λν μ€ μ€λ₯: {e}") | |
| session['like_status'] = {} | |
| return jsonify({ | |
| 'success': True, | |
| 'username': username | |
| }) | |
| def logout(): | |
| session.pop('token', None) | |
| session.pop('username', None) | |
| session.pop('like_status', None) | |
| return jsonify({'success': True}) | |
| def get_urls(): | |
| like_status = session.get('like_status', {}) | |
| results = [] | |
| for url in HUGGINGFACE_URLS: | |
| title = extract_title(url) | |
| model_info = extract_model_info(url) | |
| if not model_info: | |
| continue | |
| # μ’μμ μν νμΈ | |
| is_liked = like_status.get(url, False) | |
| results.append({ | |
| 'url': url, | |
| 'title': title, | |
| 'model_info': model_info, | |
| 'is_liked': is_liked | |
| }) | |
| return jsonify(results) | |
| def toggle_like(): | |
| if 'token' not in session: | |
| return jsonify({'success': False, 'message': 'λ‘κ·ΈμΈμ΄ νμν©λλ€.'}) | |
| data = request.json | |
| url = data.get('url') | |
| if not url: | |
| return jsonify({'success': False, 'message': 'URLμ΄ νμν©λλ€.'}) | |
| token = session['token'] | |
| # URLμμ λͺ¨λΈ μ 보 μΆμΆ | |
| model_info = extract_model_info(url) | |
| if not model_info: | |
| return jsonify({'success': False, 'message': 'μλͺ»λ URL νμμ λλ€.'}) | |
| # νμ¬ μ’μμ μν νμΈ | |
| like_status = session.get('like_status', {}) | |
| current_status = like_status.get(url, False) | |
| # API μμ²μ μν ν€λ λ° λ°μ΄ν° μ€μ | |
| headers = { | |
| "Authorization": f"Bearer {token}", | |
| "Content-Type": "application/json" | |
| } | |
| # API μλν¬μΈνΈ λ° λ©μλ κ²°μ | |
| # Hugging Face APIμμ λͺ¨λΈ/μ€νμ΄μ€λ₯Ό μ’μμ/μ·¨μνλ μλν¬μΈνΈ | |
| if model_info['type'] == 'spaces': | |
| api_url = f"https://huggingface.co/api/spaces/{model_info['full_id']}/like" | |
| else: | |
| api_url = f"https://huggingface.co/api/models/{model_info['full_id']}/like" | |
| # νμ¬ μνμ λ°λλ‘ λ³κ²½ | |
| try: | |
| if current_status: | |
| # μ’μμ μ·¨μ (DELETE μμ²) | |
| response = requests.delete(api_url, headers=headers) | |
| else: | |
| # μ’μμ μΆκ° (POST μμ²) | |
| response = requests.post(api_url, headers=headers, json={}) | |
| # μλ΅ νμΈ | |
| if response.status_code in [200, 201, 204]: | |
| # μ±κ³΅μ μΌλ‘ λ³κ²½λλ©΄ μΈμ μν μ λ°μ΄νΈ | |
| new_status = not current_status | |
| like_status[url] = new_status | |
| session['like_status'] = like_status | |
| return jsonify({ | |
| 'success': True, | |
| 'is_liked': new_status, | |
| 'message': 'μ’μμλ₯Ό μΆκ°νμ΅λλ€.' if new_status else 'μ’μμλ₯Ό μ·¨μνμ΅λλ€.' | |
| }) | |
| else: | |
| # API μλ΅ μ€λ₯ | |
| error_message = f"Hugging Face API μ€λ₯ (μν μ½λ: {response.status_code})" | |
| try: | |
| error_data = response.json() | |
| if 'error' in error_data: | |
| error_message += f": {error_data['error']}" | |
| except: | |
| pass | |
| logger.error(f"{error_message}, μλ΅: {response.text}") | |
| return jsonify({'success': False, 'message': error_message}) | |
| except Exception as e: | |
| logger.error(f"μ’μμ μν λ³κ²½ μ€ μ€λ₯: {e}") | |
| return jsonify({ | |
| 'success': False, | |
| 'message': f'μ’μμ μν λ³κ²½ μ€ μ€λ₯: {str(e)}' | |
| }) | |
| def refresh_likes(): | |
| if 'token' not in session: | |
| return jsonify({'success': False, 'message': 'λ‘κ·ΈμΈμ΄ νμν©λλ€.'}) | |
| try: | |
| # μΉ μ€ν¬λνμΌλ‘ μ’μμ μν μλ‘κ³ μΉ¨ | |
| like_status = scrape_all_like_status(session['token']) | |
| session['like_status'] = like_status | |
| return jsonify({ | |
| 'success': True, | |
| 'message': 'μ’μμ μνκ° μλ‘κ³ μΉ¨λμμ΅λλ€.', | |
| 'like_status': like_status | |
| }) | |
| except Exception as e: | |
| logger.error(f"μ’μμ μν μλ‘κ³ μΉ¨ μ€ μ€λ₯: {e}") | |
| return jsonify({ | |
| 'success': False, | |
| 'message': f'μ’μμ μν μλ‘κ³ μΉ¨ μ€ μ€λ₯: {str(e)}' | |
| }) | |
| def session_status(): | |
| return jsonify({ | |
| 'logged_in': 'username' in session, | |
| 'username': session.get('username') | |
| }) | |
| if __name__ == '__main__': | |
| os.makedirs('templates', exist_ok=True) | |
| with open('templates/index.html', 'w', encoding='utf-8') as f: | |
| f.write(''' | |
| <!DOCTYPE html> | |
| <html lang="ko"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Hugging Face URL μΉ΄λ 리μ€νΈ</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| line-height: 1.6; | |
| margin: 0; | |
| padding: 0; | |
| color: #333; | |
| background-color: #f4f5f7; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 1rem; | |
| } | |
| .header { | |
| background-color: #fff; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| } | |
| .user-controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .filter-controls { | |
| background-color: #fff; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| input[type="password"], | |
| input[type="text"] { | |
| padding: 0.5rem; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| margin-right: 5px; | |
| } | |
| button { | |
| padding: 0.5rem 1rem; | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| transition: background-color 0.2s; | |
| } | |
| button:hover { | |
| background-color: #45a049; | |
| } | |
| button.refresh { | |
| background-color: #2196F3; | |
| } | |
| button.refresh:hover { | |
| background-color: #0b7dda; | |
| } | |
| button.logout { | |
| background-color: #f44336; | |
| } | |
| button.logout:hover { | |
| background-color: #d32f2f; | |
| } | |
| .token-help { | |
| margin-top: 0.5rem; | |
| font-size: 0.8rem; | |
| color: #666; | |
| } | |
| .token-help a { | |
| color: #4CAF50; | |
| text-decoration: none; | |
| } | |
| .token-help a:hover { | |
| text-decoration: underline; | |
| } | |
| .cards-container { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 1rem; | |
| } | |
| .card { | |
| border: 1px solid #ddd; | |
| border-radius: 8px; | |
| padding: 1rem; | |
| width: 300px; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| position: relative; | |
| background-color: #fff; | |
| transition: all 0.3s ease; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 5px 15px rgba(0,0,0,0.1); | |
| } | |
| .card.liked { | |
| border-color: #ff4757; | |
| background-color: #ffebee; | |
| } | |
| .card-header { | |
| margin-bottom: 0.5rem; | |
| padding-right: 40px; /* μ’μμ λ²νΌ κ³΅κ° */ | |
| } | |
| .card-title { | |
| font-size: 1.2rem; | |
| margin: 0 0 0.5rem 0; | |
| color: #333; | |
| } | |
| .card a { | |
| text-decoration: none; | |
| color: #2980b9; | |
| word-break: break-all; | |
| display: block; | |
| font-size: 0.9rem; | |
| } | |
| .card a:hover { | |
| text-decoration: underline; | |
| } | |
| .like-button { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| width: 30px; | |
| height: 30px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 50%; | |
| border: none; | |
| background: transparent; | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| color: #ddd; | |
| } | |
| .like-button:hover { | |
| transform: scale(1.2); | |
| } | |
| .like-button.liked { | |
| color: #ff4757; | |
| } | |
| .like-badge { | |
| position: absolute; | |
| top: -5px; | |
| left: -5px; | |
| background-color: #ff4757; | |
| color: white; | |
| padding: 0.2rem 0.5rem; | |
| border-radius: 4px; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| } | |
| .like-status { | |
| background-color: #fff; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| margin-bottom: 1rem; | |
| box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| display: none; | |
| } | |
| .like-status strong { | |
| color: #ff4757; | |
| } | |
| .status-message { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| padding: 1rem; | |
| border-radius: 8px; | |
| display: none; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); | |
| z-index: 1000; | |
| max-width: 300px; | |
| } | |
| .success { | |
| background-color: #4CAF50; | |
| color: white; | |
| } | |
| .error { | |
| background-color: #f44336; | |
| color: white; | |
| } | |
| .loading { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(255, 255, 255, 0.8); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| } | |
| .spinner { | |
| width: 40px; | |
| height: 40px; | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #3498db; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .filter-toggle { | |
| display: flex; | |
| } | |
| .filter-toggle button { | |
| margin-right: 0.5rem; | |
| background-color: #f0f0f0; | |
| color: #333; | |
| } | |
| .filter-toggle button.active { | |
| background-color: #4CAF50; | |
| color: white; | |
| } | |
| .login-section { | |
| margin-top: 1rem; | |
| } | |
| .logged-in-section { | |
| display: none; | |
| margin-top: 1rem; | |
| } | |
| .note { | |
| padding: 0.5rem; | |
| background-color: #fffde7; | |
| border-left: 3px solid #ffd600; | |
| margin-bottom: 1rem; | |
| font-size: 0.9rem; | |
| } | |
| @media (max-width: 768px) { | |
| .user-controls { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .user-controls > div { | |
| margin-bottom: 1rem; | |
| } | |
| .filter-controls { | |
| flex-direction: column; | |
| } | |
| .filter-controls > div { | |
| margin-bottom: 0.5rem; | |
| } | |
| .card { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <div class="user-controls"> | |
| <div> | |
| <span>νκΉ νμ΄μ€ κ³μ : </span> | |
| <span id="currentUser">λ‘κ·ΈμΈλμ§ μμ</span> | |
| </div> | |
| <div id="loginSection" class="login-section"> | |
| <input type="password" id="tokenInput" placeholder="νκΉ νμ΄μ€ API ν ν° μ λ ₯" /> | |
| <button id="loginButton">μΈμ¦νκΈ°</button> | |
| <div class="token-help"> | |
| API ν ν°μ <a href="https://huggingface.co/settings/tokens" target="_blank">νκΉ νμ΄μ€ ν ν° νμ΄μ§</a>μμ μμ±ν μ μμ΅λλ€. | |
| </div> | |
| </div> | |
| <div id="loggedInSection" class="logged-in-section"> | |
| <button id="refreshButton" class="refresh">μλ‘κ³ μΉ¨</button> | |
| <button id="logoutButton" class="logout">λ‘κ·Έμμ</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="note"> | |
| <p><strong>μ°Έκ³ :</strong> μ΄ νμ΄μ§λ μΉ μ€ν¬λν λ°©μμΌλ‘ μ’μμ μνλ₯Ό κ°μ Έμ΅λλ€. μ’μμ μνκ° μ ννμ§ μκ±°λ μ§μ°λ μ μμ΅λλ€. 'μλ‘κ³ μΉ¨' λ²νΌμ ν΄λ¦νμ¬ μ΅μ μνλ₯Ό κ°μ Έμ¬ μ μμ΅λλ€.</p> | |
| </div> | |
| <div id="likeStatus" class="like-status"> | |
| <div id="likeStatsText">μ΄ <span id="totalUrlCount">0</span>κ° μ€ <strong><span id="likedUrlCount">0</span>κ°</strong>μ URLμ μ’μμ νμ΅λλ€.</div> | |
| </div> | |
| <div class="filter-controls"> | |
| <div> | |
| <input type="text" id="searchInput" placeholder="URL λλ μ λͺ©μΌλ‘ κ²μ" style="width: 300px;" /> | |
| </div> | |
| <div class="filter-toggle"> | |
| <button id="allUrlsBtn" class="active">μ 체 보기</button> | |
| <button id="likedUrlsBtn">μ’μμλ§ λ³΄κΈ°</button> | |
| </div> | |
| </div> | |
| <div id="statusMessage" class="status-message"></div> | |
| <div id="loadingIndicator" class="loading"> | |
| <div class="spinner"></div> | |
| </div> | |
| <div id="cardsContainer" class="cards-container"></div> | |
| </div> | |
| <script> | |
| // DOM μμ μ°Έμ‘° | |
| const elements = { | |
| tokenInput: document.getElementById('tokenInput'), | |
| loginButton: document.getElementById('loginButton'), | |
| logoutButton: document.getElementById('logoutButton'), | |
| refreshButton: document.getElementById('refreshButton'), | |
| currentUser: document.getElementById('currentUser'), | |
| cardsContainer: document.getElementById('cardsContainer'), | |
| loadingIndicator: document.getElementById('loadingIndicator'), | |
| statusMessage: document.getElementById('statusMessage'), | |
| searchInput: document.getElementById('searchInput'), | |
| loginSection: document.getElementById('loginSection'), | |
| loggedInSection: document.getElementById('loggedInSection'), | |
| likeStatus: document.getElementById('likeStatus'), | |
| totalUrlCount: document.getElementById('totalUrlCount'), | |
| likedUrlCount: document.getElementById('likedUrlCount'), | |
| allUrlsBtn: document.getElementById('allUrlsBtn'), | |
| likedUrlsBtn: document.getElementById('likedUrlsBtn') | |
| }; | |
| // μ ν리μΌμ΄μ μν | |
| const state = { | |
| username: null, | |
| allURLs: [], | |
| isLoading: false, | |
| viewMode: 'all' // 'all' λλ 'liked' | |
| }; | |
| // λ‘λ© μν νμ ν¨μ | |
| function setLoading(isLoading) { | |
| state.isLoading = isLoading; | |
| elements.loadingIndicator.style.display = isLoading ? 'flex' : 'none'; | |
| } | |
| // μν λ©μμ§ νμ ν¨μ | |
| function showMessage(message, isError = false) { | |
| elements.statusMessage.textContent = message; | |
| elements.statusMessage.className = `status-message ${isError ? 'error' : 'success'}`; | |
| elements.statusMessage.style.display = 'block'; | |
| // 3μ΄ ν λ©μμ§ μ¬λΌμ§ | |
| setTimeout(() => { | |
| elements.statusMessage.style.display = 'none'; | |
| }, 3000); | |
| } | |
| // API μ€λ₯ μ²λ¦¬ ν¨μ | |
| async function handleApiResponse(response) { | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| throw new Error(`API μ€λ₯ (${response.status}): ${errorText}`); | |
| } | |
| return response.json(); | |
| } | |
| // μ’μμ ν΅κ³ μ λ°μ΄νΈ | |
| function updateLikeStats() { | |
| const totalCount = state.allURLs.length; | |
| const likedCount = state.allURLs.filter(item => item.is_liked).length; | |
| elements.totalUrlCount.textContent = totalCount; | |
| elements.likedUrlCount.textContent = likedCount; | |
| } | |
| // μΈμ μν νμΈ | |
| async function checkSessionStatus() { | |
| try { | |
| const response = await fetch('/api/session-status'); | |
| const data = await handleApiResponse(response); | |
| if (data.logged_in) { | |
| state.username = data.username; | |
| elements.currentUser.textContent = data.username; | |
| elements.loginSection.style.display = 'none'; | |
| elements.loggedInSection.style.display = 'block'; | |
| elements.likeStatus.style.display = 'block'; | |
| // URL λͺ©λ‘ λ‘λ | |
| loadUrls(); | |
| } | |
| } catch (error) { | |
| console.error('μΈμ μν νμΈ μ€λ₯:', error); | |
| } | |
| } | |
| // λ‘κ·ΈμΈ μ²λ¦¬ | |
| async function login(token) { | |
| if (!token.trim()) { | |
| showMessage('ν ν°μ μ λ ₯ν΄μ£ΌμΈμ.', true); | |
| return; | |
| } | |
| setLoading(true); | |
| try { | |
| const formData = new FormData(); | |
| formData.append('token', token); | |
| const response = await fetch('/api/login', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await handleApiResponse(response); | |
| if (data.success) { | |
| state.username = data.username; | |
| elements.currentUser.textContent = state.username; | |
| elements.loginSection.style.display = 'none'; | |
| elements.loggedInSection.style.display = 'block'; | |
| elements.likeStatus.style.display = 'block'; | |
| showMessage(`${state.username}λμΌλ‘ λ‘κ·ΈμΈλμμ΅λλ€.`); | |
| // URL λͺ©λ‘ λ‘λ | |
| loadUrls(); | |
| } else { | |
| showMessage(data.message || 'λ‘κ·ΈμΈμ μ€ν¨νμ΅λλ€.', true); | |
| } | |
| } catch (error) { | |
| console.error('λ‘κ·ΈμΈ μ€λ₯:', error); | |
| showMessage(`λ‘κ·ΈμΈ μ€λ₯: ${error.message}`, true); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // λ‘κ·Έμμ μ²λ¦¬ | |
| async function logout() { | |
| setLoading(true); | |
| try { | |
| const response = await fetch('/api/logout', { | |
| method: 'POST' | |
| }); | |
| const data = await handleApiResponse(response); | |
| if (data.success) { | |
| state.username = null; | |
| state.allURLs = []; | |
| elements.currentUser.textContent = 'λ‘κ·ΈμΈλμ§ μμ'; | |
| elements.tokenInput.value = ''; | |
| elements.loginSection.style.display = 'block'; | |
| elements.loggedInSection.style.display = 'none'; | |
| elements.likeStatus.style.display = 'none'; | |
| showMessage('λ‘κ·Έμμλμμ΅λλ€.'); | |
| // μΉ΄λ μ΄κΈ°ν | |
| elements.cardsContainer.innerHTML = ''; | |
| } | |
| } catch (error) { | |
| console.error('λ‘κ·Έμμ μ€λ₯:', error); | |
| showMessage(`λ‘κ·Έμμ μ€λ₯: ${error.message}`, true); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // μ’μμ μν μλ‘κ³ μΉ¨ | |
| async function refreshLikes() { | |
| setLoading(true); | |
| try { | |
| const response = await fetch('/api/refresh-likes', { | |
| method: 'POST' | |
| }); | |
| const data = await handleApiResponse(response); | |
| if (data.success) { | |
| // URL λͺ©λ‘ λ€μ λ‘λ | |
| loadUrls(); | |
| showMessage('μ’μμ μνκ° μλ‘κ³ μΉ¨λμμ΅λλ€.'); | |
| } else { | |
| showMessage(data.message || 'μ’μμ μν μλ‘κ³ μΉ¨μ μ€ν¨νμ΅λλ€.', true); | |
| } | |
| } catch (error) { | |
| console.error('μ’μμ μν μλ‘κ³ μΉ¨ μ€λ₯:', error); | |
| showMessage(`μ’μμ μν μλ‘κ³ μΉ¨ μ€λ₯: ${error.message}`, true); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // URL λͺ©λ‘ λ‘λ | |
| async function loadUrls() { | |
| setLoading(true); | |
| try { | |
| const response = await fetch('/api/urls'); | |
| const data = await handleApiResponse(response); | |
| state.allURLs = data; | |
| updateLikeStats(); | |
| renderCards(); | |
| } catch (error) { | |
| console.error('URL λͺ©λ‘ λ‘λ μ€λ₯:', error); | |
| showMessage(`URL λͺ©λ‘ λ‘λ μ€λ₯: ${error.message}`, true); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| // μ’μμ ν κΈ | |
| async function toggleLike(url) { | |
| try { | |
| const response = await fetch('/api/toggle-like', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ url }) | |
| }); | |
| const data = await handleApiResponse(response); | |
| if (data.success) { | |
| // URL κ°μ²΄ μ°ΎκΈ° | |
| const urlObj = state.allURLs.find(item => item.url === url); | |
| if (urlObj) { | |
| urlObj.is_liked = data.is_liked; | |
| updateLikeStats(); | |
| renderCards(); | |
| } | |
| showMessage(data.message); | |
| } else { | |
| showMessage(data.message || 'μ’μμ μν λ³κ²½μ μ€ν¨νμ΅λλ€.', true); | |
| } | |
| } catch (error) { | |
| console.error('μ’μμ ν κΈ μ€λ₯:', error); | |
| showMessage(`μ’μμ ν κΈ μ€λ₯: ${error.message}`, true); | |
| } | |
| } | |
| // μΉ΄λ λ λλ§ | |
| function renderCards() { | |
| elements.cardsContainer.innerHTML = ''; | |
| let urlsToShow = state.allURLs; | |
| // κ²μμ΄λ‘ νν°λ§ | |
| const searchTerm = elements.searchInput.value.trim().toLowerCase(); | |
| if (searchTerm) { | |
| urlsToShow = urlsToShow.filter(item => | |
| item.url.toLowerCase().includes(searchTerm) || | |
| item.title.toLowerCase().includes(searchTerm) | |
| ); | |
| } | |
| // 보기 λͺ¨λλ‘ νν°λ§ (μ 체 λλ μ’μμλ§) | |
| if (state.viewMode === 'liked') { | |
| urlsToShow = urlsToShow.filter(item => item.is_liked); | |
| } | |
| if (urlsToShow.length === 0) { | |
| const emptyMessage = document.createElement('div'); | |
| emptyMessage.textContent = 'νμν URLμ΄ μμ΅λλ€.'; | |
| emptyMessage.style.padding = '1rem'; | |
| emptyMessage.style.width = '100%'; | |
| emptyMessage.style.textAlign = 'center'; | |
| elements.cardsContainer.appendChild(emptyMessage); | |
| return; | |
| } | |
| // μΉ΄λ μμ± | |
| urlsToShow.forEach(item => { | |
| const card = document.createElement('div'); | |
| card.className = `card ${item.is_liked ? 'liked' : ''}`; | |
| if (item.is_liked) { | |
| const badge = document.createElement('div'); | |
| badge.className = 'like-badge'; | |
| badge.textContent = 'μ’μμ'; | |
| card.appendChild(badge); | |
| } | |
| const cardContent = ` | |
| <div class="card-header"> | |
| <h3 class="card-title">${item.title}</h3> | |
| <button class="like-button ${item.is_liked ? 'liked' : ''}" data-url="${item.url}"> | |
| β€ | |
| </button> | |
| </div> | |
| <div class="card-body"> | |
| <a href="${item.url}" target="_blank">${item.url}</a> | |
| <div style="margin-top: 0.5rem;"> | |
| <span>μμ μ: ${item.model_info.owner}</span> | |
| </div> | |
| <div> | |
| <span>μ μ₯μ: ${item.model_info.repo}</span> | |
| </div> | |
| <div> | |
| <span>μ ν: ${item.model_info.type}</span> | |
| </div> | |
| </div> | |
| `; | |
| card.innerHTML = cardContent; | |
| elements.cardsContainer.appendChild(card); | |
| // μ’μμ λ²νΌ μ΄λ²€νΈ μΆκ° | |
| const likeButton = card.querySelector('.like-button'); | |
| likeButton.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const url = e.target.dataset.url; | |
| toggleLike(url); | |
| }); | |
| }); | |
| } | |
| // μ΄λ²€νΈ 리μ€λ λ±λ‘ | |
| function registerEventListeners() { | |
| // λ‘κ·ΈμΈ λ²νΌ | |
| elements.loginButton.addEventListener('click', () => { | |
| login(elements.tokenInput.value); | |
| }); | |
| // μν° ν€λ‘ λ‘κ·ΈμΈ | |
| elements.tokenInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| login(elements.tokenInput.value); | |
| } | |
| }); | |
| // λ‘κ·Έμμ λ²νΌ | |
| elements.logoutButton.addEventListener('click', logout); | |
| // μλ‘κ³ μΉ¨ λ²νΌ | |
| elements.refreshButton.addEventListener('click', refreshLikes); | |
| // κ²μ μ λ ₯ νλ | |
| elements.searchInput.addEventListener('input', renderCards); | |
| // νν° λ²νΌ - μ 체 보기 | |
| elements.allUrlsBtn.addEventListener('click', () => { | |
| elements.allUrlsBtn.classList.add('active'); | |
| elements.likedUrlsBtn.classList.remove('active'); | |
| state.viewMode = 'all'; | |
| renderCards(); | |
| }); | |
| // νν° λ²νΌ - μ’μμλ§ λ³΄κΈ° | |
| elements.likedUrlsBtn.addEventListener('click', () => { | |
| elements.likedUrlsBtn.classList.add('active'); | |
| elements.allUrlsBtn.classList.remove('active'); | |
| state.viewMode = 'liked'; | |
| renderCards(); | |
| }); | |
| } | |
| // μ΄κΈ°ν ν¨μ | |
| function init() { | |
| registerEventListeners(); | |
| checkSessionStatus(); | |
| } | |
| // μ ν리μΌμ΄μ μ΄κΈ°ν | |
| init(); | |
| </script> | |
| </body> | |
| </html> | |
| ''' | |
| ) | |
| app.run(debug=True, host='0.0.0.0', port=7860) |