Spaces:
Running
Running
from flask import Flask, render_template, request, jsonify, session | |
import os | |
from datetime import timedelta | |
app = Flask(__name__) | |
app.secret_key = os.urandom(24) | |
app.permanent_session_lifetime = timedelta(days=7) | |
# Hugging Face URL λͺ©λ‘ - μΌλΆ URLμ 미리 μ’μμ μνλ‘ μ€μ | |
# 'is_liked'λ₯Ό trueλ‘ μ€μ ν URLμ νμ μ’μμ μνλ‘ νμλ¨ | |
HUGGINGFACE_URLS = [ | |
{"url": "https://huggingface.co/spaces/ginipick/Tech_Hangman_Game", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/openfree/deepseek_r1_API", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/ginipick/open_Deep-Research", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/aiqmaster/open-deep-research", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/seawolf2357/DeepSeek-R1-32b-search", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/ginigen/LLaDA", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/VIDraft/PHI4-Multimodal", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/ginigen/Ovis2-8B", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/ginigen/Graph-Mind", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/ginigen/Workflow-Canvas", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/ginigen/Design", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/ginigen/Diagram", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/ginigen/Mockup", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/ginigen/Infographic", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/ginigen/Flowchart", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/aiqcamp/FLUX-Vision", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/ginigen/VoiceClone-TTS", "is_liked": True}, | |
{"url": "https://huggingface.co/spaces/openfree/Perceptron-Network", "is_liked": False}, | |
{"url": "https://huggingface.co/spaces/openfree/Article-Generator", "is_liked": True}, | |
] | |
# URLμ λ§μ§λ§ λΆλΆμ μ λͺ©μΌλ‘ μΆμΆ | |
def extract_title(url): | |
parts = url.split("/") | |
title = parts[-1] if parts else "" | |
return title.replace("_", " ").replace("-", " ") | |
def home(): | |
return render_template('index.html') | |
def login(): | |
token = request.form.get('token', '') | |
if not token: | |
return jsonify({'success': False, 'message': 'ν ν°μ μ λ ₯ν΄μ£ΌμΈμ.'}) | |
# κ°λ¨ν ν ν° κΈΈμ΄λ§ κ²μ¬ (μ€μ λ‘λ λ 볡μ‘ν κ²μ¦ νμ) | |
if len(token) < 5: | |
return jsonify({'success': False, 'message': 'μ ν¨νμ§ μμ ν ν°μ λλ€.'}) | |
# ν ν°μ 첫 κΈμλ₯Ό μ¬μ©μ μ΄λ¦μΌλ‘ μ€μ (ν μ€νΈμ©) | |
username = f"μ¬μ©μ_{token[:3]}" | |
# μΈμ μ μ μ₯ | |
session['username'] = username | |
return jsonify({ | |
'success': True, | |
'username': username | |
}) | |
def logout(): | |
session.pop('username', None) | |
return jsonify({'success': True}) | |
def get_urls(): | |
results = [] | |
for url_item in HUGGINGFACE_URLS: | |
url = url_item["url"] | |
is_liked = url_item["is_liked"] | |
title = extract_title(url) | |
results.append({ | |
'url': url, | |
'title': title, | |
'is_liked': is_liked | |
}) | |
return jsonify(results) | |
def toggle_like(): | |
if 'username' not in session: | |
return jsonify({'success': False, 'message': 'λ‘κ·ΈμΈμ΄ νμν©λλ€.'}) | |
data = request.json | |
url = data.get('url') | |
if not url: | |
return jsonify({'success': False, 'message': 'URLμ΄ νμν©λλ€.'}) | |
# URL λͺ©λ‘μμ ν΄λΉ URL μ°ΎκΈ° | |
for url_item in HUGGINGFACE_URLS: | |
if url_item["url"] == url: | |
# μ’μμ μν ν κΈ | |
url_item["is_liked"] = not url_item["is_liked"] | |
return jsonify({ | |
'success': True, | |
'is_liked': url_item["is_liked"], | |
'message': 'μ’μμλ₯Ό μΆκ°νμ΅λλ€.' if url_item["is_liked"] else 'μ’μμλ₯Ό μ·¨μνμ΅λλ€.' | |
}) | |
return jsonify({'success': False, 'message': 'ν΄λΉ URLμ μ°Ύμ μ μμ΅λλ€.'}) | |
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.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; | |
} | |
@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="logoutButton" class="logout">λ‘κ·Έμμ</button> | |
</div> | |
</div> | |
</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'), | |
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); | |
} | |
} | |
// URL λͺ©λ‘ λ‘λ | |
async function loadUrls() { | |
setLoading(true); | |
try { | |
const response = await fetch('/api/urls'); | |
const urls = await handleApiResponse(response); | |
// URL λ° μ’μμ μν μ μ₯ | |
state.allURLs = urls; | |
// νν°λ§ λ° λ λλ§ | |
filterAndRenderCards(); | |
// μ’μμ ν΅κ³ μ λ°μ΄νΈ | |
updateLikeStats(); | |
} catch (error) { | |
console.error('URL λͺ©λ‘ λ‘λ μ€λ₯:', error); | |
showMessage(`URL λ‘λ μ€λ₯: ${error.message}`, true); | |
} finally { | |
setLoading(false); | |
} | |
} | |
// νν°λ§ λ° μΉ΄λ λ λλ§ | |
function filterAndRenderCards() { | |
const searchText = elements.searchInput.value.toLowerCase(); | |
// νν°λ§ μ μ© | |
const filteredUrls = state.allURLs.filter(item => { | |
const { url, title, is_liked } = item; | |
// μ’μμ νν°λ§ (μ’μμλ§ λ³΄κΈ° λͺ¨λ) | |
if (state.viewMode === 'liked' && !is_liked) { | |
return false; | |
} | |
// κ²μ νν°λ§ | |
if (searchText && !url.toLowerCase().includes(searchText) && !title.toLowerCase().includes(searchText)) { | |
return false; | |
} | |
return true; | |
}); | |
renderCards(filteredUrls); | |
} | |
// μ’μμ ν κΈ | |
async function toggleLike(url, card) { | |
if (!state.username) { | |
showMessage('μ’μμλ₯Ό νλ €λ©΄ νκΉ νμ΄μ€ API ν ν°μΌλ‘ μΈμ¦μ΄ νμν©λλ€.', true); | |
return; | |
} | |
setLoading(true); | |
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 urlItem = state.allURLs.find(item => item.url === url); | |
if (urlItem) { | |
// μ’μμ μν μ λ°μ΄νΈ | |
urlItem.is_liked = data.is_liked; | |
// μΉ΄λ UI μ λ°μ΄νΈ | |
if (data.is_liked) { | |
card.classList.add('liked'); | |
const likeBtn = card.querySelector('.like-button'); | |
if (likeBtn) likeBtn.classList.add('liked'); | |
// μ’μμ λ°°μ§ μΆκ° | |
if (!card.querySelector('.like-badge')) { | |
const likeBadge = document.createElement('div'); | |
likeBadge.className = 'like-badge'; | |
likeBadge.textContent = 'μ’μμ'; | |
card.appendChild(likeBadge); | |
} | |
} else { | |
card.classList.remove('liked'); | |
const likeBtn = card.querySelector('.like-button'); | |
if (likeBtn) likeBtn.classList.remove('liked'); | |
// μ’μμ λ°°μ§ μ κ±° | |
const likeBadge = card.querySelector('.like-badge'); | |
if (likeBadge) card.removeChild(likeBadge); | |
} | |
} | |
showMessage(data.message); | |
// μ’μμ ν΅κ³ μ λ°μ΄νΈ | |
updateLikeStats(); | |
// μ’μμλ§ λ³΄κΈ° λͺ¨λμΈ κ²½μ° λͺ©λ‘ λ€μ νν°λ§ | |
if (state.viewMode === 'liked') { | |
filterAndRenderCards(); | |
} | |
} else { | |
showMessage(data.message || 'μ’μμ μ²λ¦¬μ μ€ν¨νμ΅λλ€.', true); | |
} | |
} catch (error) { | |
console.error('μ’μμ ν κΈ μ€λ₯:', error); | |
showMessage(`μ’μμ μ²λ¦¬ μ€λ₯: ${error.message}`, true); | |
} finally { | |
setLoading(false); | |
} | |
} | |
// μΉ΄λ λ λλ§ | |
function renderCards(urls) { | |
elements.cardsContainer.innerHTML = ''; | |
if (!urls || urls.length === 0) { | |
const noResultsMsg = document.createElement('p'); | |
noResultsMsg.textContent = 'νμν URLμ΄ μμ΅λλ€.'; | |
noResultsMsg.style.padding = '1rem'; | |
noResultsMsg.style.fontStyle = 'italic'; | |
elements.cardsContainer.appendChild(noResultsMsg); | |
return; | |
} | |
urls.forEach(item => { | |
const { url, title, is_liked } = item; | |
// μΉ΄λ μμ± | |
const card = document.createElement('div'); | |
card.className = `card ${is_liked ? 'liked' : ''}`; | |
// μΉ΄λ ν€λ | |
const cardHeader = document.createElement('div'); | |
cardHeader.className = 'card-header'; | |
// μ λͺ© | |
const titleEl = document.createElement('h3'); | |
titleEl.className = 'card-title'; | |
titleEl.textContent = title; | |
cardHeader.appendChild(titleEl); | |
card.appendChild(cardHeader); | |
// URL λ§ν¬ | |
const linkEl = document.createElement('a'); | |
linkEl.href = url; | |
linkEl.textContent = url; | |
linkEl.target = '_blank'; | |
card.appendChild(linkEl); | |
// μ’μμ λ²νΌ | |
const likeBtn = document.createElement('button'); | |
likeBtn.className = `like-button ${is_liked ? 'liked' : ''}`; | |
likeBtn.innerHTML = 'β₯'; | |
likeBtn.title = is_liked ? 'μ’μμ μ·¨μ' : 'μ’μμ'; | |
likeBtn.addEventListener('click', (e) => { | |
e.preventDefault(); | |
toggleLike(url, card); | |
}); | |
card.appendChild(likeBtn); | |
// μ’μμ λ°°μ§ (μ’μμ μνμΌ λλ§) | |
if (is_liked) { | |
const likeBadge = document.createElement('div'); | |
likeBadge.className = 'like-badge'; | |
likeBadge.textContent = 'μ’μμ'; | |
card.appendChild(likeBadge); | |
} | |
// μΉ΄λ μΆκ° | |
elements.cardsContainer.appendChild(card); | |
}); | |
} | |
// λ·° λͺ¨λ λ³κ²½ | |
function changeViewMode(mode) { | |
state.viewMode = mode; | |
// λ²νΌ νμ±ν μν μ λ°μ΄νΈ | |
elements.allUrlsBtn.classList.toggle('active', mode === 'all'); | |
elements.likedUrlsBtn.classList.toggle('active', mode === 'liked'); | |
// μΉ΄λ λ€μ λ λλ§ | |
filterAndRenderCards(); | |
} | |
// μ΄λ²€νΈ 리μ€λ μ€μ | |
elements.loginButton.addEventListener('click', () => { | |
login(elements.tokenInput.value); | |
}); | |
elements.logoutButton.addEventListener('click', logout); | |
// μν° ν€λ‘ λ‘κ·ΈμΈ κ°λ₯νκ² | |
elements.tokenInput.addEventListener('keypress', (event) => { | |
if (event.key === 'Enter') { | |
login(elements.tokenInput.value); | |
} | |
}); | |
// κ²μ μ΄λ²€νΈ 리μ€λ | |
elements.searchInput.addEventListener('input', () => { | |
// λλ°μ΄μ± (μ λ ₯ μ§μ° μ²λ¦¬) | |
clearTimeout(state.searchTimeout); | |
state.searchTimeout = setTimeout(filterAndRenderCards, 300); | |
}); | |
// νν° λ²νΌ μ΄λ²€νΈ 리μ€λ | |
elements.allUrlsBtn.addEventListener('click', () => changeViewMode('all')); | |
elements.likedUrlsBtn.addEventListener('click', () => changeViewMode('liked')); | |
// μ΄κΈ°ν | |
checkSessionStatus(); | |
</script> | |
</body> | |
</html> | |
''') | |
# νκΉ νμ΄μ€ μ€νμ΄μ€μμλ 7860 ν¬νΈ μ¬μ© | |
app.run(host='0.0.0.0', port=7860) |