seawolf2357's picture
Update app.py
5423956 verified
raw
history blame
29.9 kB
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
@app.route('/')
def home():
return render_template('index.html')
@app.route('/api/login', methods=['POST'])
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
})
@app.route('/api/logout', methods=['POST'])
def logout():
session.pop('token', None)
session.pop('username', None)
session.pop('like_status', None)
return jsonify({'success': True})
@app.route('/api/urls', methods=['GET'])
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)
@app.route('/api/toggle-like', methods=['POST'])
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 'μ’‹μ•„μš”λ₯Ό μ·¨μ†Œν–ˆμŠ΅λ‹ˆλ‹€.'
})
@app.route('/api/refresh-likes', methods=['POST'])
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)}'
})
@app.route('/api/session-status', methods=['GET'])
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)