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์ด ํ์ํฉ๋๋ค.'}) | |
# ํ์ฌ ์ข์์ ์ํ ํ์ธ | |
like_status = session.get('like_status', {}) | |
current_status = like_status.get(url, False) | |
# ์ค์ ๋ก๋ ์ฌ๊ธฐ์ ํ๊น ํ์ด์ค API๋ฅผ ํธ์ถํ์ฌ ์ข์์ ์ํ๋ฅผ ๋ณ๊ฒฝํด์ผ ํฉ๋๋ค | |
# ํ์ฌ๋ ์์๋ก ์ธ์ ์๋ง ์ํ๋ฅผ ์ ์ฅํฉ๋๋ค | |
like_status[url] = not current_status | |
session['like_status'] = like_status | |
return jsonify({ | |
'success': True, | |
'is_liked': like_status[url], | |
'message': '์ข์์๋ฅผ ์ถ๊ฐํ์ต๋๋ค.' if like_status[url] else '์ข์์๋ฅผ ์ทจ์ํ์ต๋๋ค.' | |
}) | |
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) |