Spaces:
Sleeping
Sleeping
| from flask import Flask, render_template, request, jsonify, session | |
| import requests | |
| 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 ๋ชฉ๋ก | |
| 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: | |
| print(f"ํ ํฐ ๊ฒ์ฆ ์ค๋ฅ: {e}") | |
| return False, None | |
| 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 | |
| return jsonify({ | |
| 'success': True, | |
| 'username': username | |
| }) | |
| def logout(): | |
| session.pop('token', None) | |
| session.pop('username', None) | |
| return jsonify({'success': True}) | |
| def get_urls(): | |
| results = [] | |
| for url in HUGGINGFACE_URLS: | |
| title = extract_title(url) | |
| model_info = extract_model_info(url) | |
| if not model_info: | |
| continue | |
| results.append({ | |
| 'url': url, | |
| 'title': title, | |
| 'model_info': model_info | |
| }) | |
| return jsonify(results) | |
| def session_status(): | |
| return jsonify({ | |
| 'logged_in': 'token' 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; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 1rem; | |
| } | |
| .header { | |
| background-color: #f0f0f0; | |
| padding: 1rem; | |
| border-radius: 5px; | |
| margin-bottom: 1rem; | |
| } | |
| .stats-bar { | |
| background-color: #e9f7ef; | |
| padding: 1rem; | |
| border-radius: 5px; | |
| margin-bottom: 1rem; | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| } | |
| .stats-bar .liked-count { | |
| font-weight: bold; | |
| color: #e74c3c; | |
| } | |
| .user-controls { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .filter-controls { | |
| background-color: #f8f9fa; | |
| padding: 1rem; | |
| border-radius: 5px; | |
| margin-bottom: 1rem; | |
| } | |
| input[type="password"], | |
| input[type="text"] { | |
| padding: 0.5rem; | |
| border: 1px solid #ccc; | |
| border-radius: 4px; | |
| margin-right: 5px; | |
| } | |
| button { | |
| padding: 0.5rem 1rem; | |
| background-color: #4CAF50; | |
| color: white; | |
| border: none; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| } | |
| 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: #e74c3c; | |
| background-color: #fff9f9; | |
| } | |
| .card a { | |
| text-decoration: none; | |
| color: #2980b9; | |
| word-break: break-all; | |
| display: block; | |
| margin-top: 0.5rem; | |
| } | |
| .card a:hover { | |
| text-decoration: underline; | |
| } | |
| .card-title { | |
| margin-top: 0; | |
| color: #333; | |
| font-size: 1.2rem; | |
| padding-right: 30px; /* ์ข์์ ๋ฒํผ ๊ณต๊ฐ ํ๋ณด */ | |
| } | |
| .like-button { | |
| position: absolute; | |
| top: 1rem; | |
| right: 1rem; | |
| border: none; | |
| background: transparent; | |
| font-size: 1.8rem; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| z-index: 2; | |
| } | |
| .like-button:hover { | |
| transform: scale(1.2); | |
| } | |
| .like-button.liked { | |
| color: #e74c3c; | |
| text-shadow: 0 0 5px rgba(231, 76, 60, 0.3); | |
| } | |
| .like-button.not-liked { | |
| color: #ccc; | |
| } | |
| .like-label { | |
| position: absolute; | |
| top: 10px; | |
| left: 10px; | |
| background-color: #e74c3c; | |
| color: white; | |
| padding: 2px 8px; | |
| border-radius: 10px; | |
| font-size: 0.7rem; | |
| font-weight: bold; | |
| display: none; | |
| } | |
| .card.liked .like-label { | |
| display: block; | |
| } | |
| .status-message { | |
| padding: 1rem; | |
| border-radius: 4px; | |
| margin-bottom: 1rem; | |
| display: none; | |
| } | |
| .success { | |
| background-color: #dff0d8; | |
| color: #3c763d; | |
| } | |
| .error { | |
| background-color: #f2dede; | |
| color: #a94442; | |
| } | |
| .loading { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(255, 255, 255, 0.7); | |
| display: none; | |
| justify-content: center; | |
| align-items: center; | |
| z-index: 1000; | |
| font-size: 1.5rem; | |
| } | |
| .login-section { | |
| margin-top: 1rem; | |
| } | |
| .logged-in-section { | |
| display: none; | |
| margin-top: 1rem; | |
| } | |
| .view-toggle { | |
| margin-bottom: 1rem; | |
| display: flex; | |
| gap: 0.5rem; | |
| } | |
| .view-toggle button { | |
| background-color: #f8f9fa; | |
| color: #333; | |
| border: 1px solid #ddd; | |
| } | |
| .view-toggle button.active { | |
| background-color: #4CAF50; | |
| color: white; | |
| } | |
| @media (max-width: 768px) { | |
| .user-controls { | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .user-controls > div { | |
| margin-bottom: 1rem; | |
| } | |
| .card { | |
| width: 100%; | |
| } | |
| .stats-bar { | |
| flex-direction: column; | |
| gap: 0.5rem; | |
| align-items: flex-start; | |
| } | |
| } | |
| </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="statsBar" class="stats-bar" style="display: none;"> | |
| <div> | |
| ์ด <span id="totalCount">0</span>๊ฐ ์ค <span id="likedCount" class="liked-count">0</span>๊ฐ ์ข์์ ํจ | |
| </div> | |
| <div class="view-toggle"> | |
| <button id="allViewBtn" class="active">์ ์ฒด ๋ณด๊ธฐ</button> | |
| <button id="likedViewBtn">์ข์์๋ง ๋ณด๊ธฐ</button> | |
| </div> | |
| </div> | |
| <div class="filter-controls"> | |
| <label> | |
| <input type="text" id="searchInput" placeholder="URL ๋๋ ์ ๋ชฉ์ผ๋ก ๊ฒ์" style="width: 250px;" /> | |
| </label> | |
| </div> | |
| <div id="statusMessage" class="status-message"></div> | |
| <div id="loadingIndicator" class="loading">์ฒ๋ฆฌ ์ค...</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'), | |
| statsBar: document.getElementById('statsBar'), | |
| totalCount: document.getElementById('totalCount'), | |
| likedCount: document.getElementById('likedCount'), | |
| allViewBtn: document.getElementById('allViewBtn'), | |
| likedViewBtn: document.getElementById('likedViewBtn') | |
| }; | |
| // ์ ํ๋ฆฌ์ผ์ด์ ์ํ | |
| const state = { | |
| username: null, | |
| likedURLs: {}, | |
| allURLs: [], | |
| isLoading: false, | |
| viewMode: 'all' // 'all' ๋๋ 'liked' | |
| }; | |
| // ๋ก์ปฌ ์คํ ๋ฆฌ์ง ํค | |
| function getLikesStorageKey(username) { | |
| return `hf_local_likes_${username}`; | |
| } | |
| // ๋ก์ปฌ ์คํ ๋ฆฌ์ง์์ ์ข์์ ์ ๋ณด ๋ก๋ | |
| function loadLikesFromStorage() { | |
| if (!state.username) return {}; | |
| const key = getLikesStorageKey(state.username); | |
| const savedLikes = localStorage.getItem(key); | |
| return savedLikes ? JSON.parse(savedLikes) : {}; | |
| } | |
| // ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ข์์ ์ ๋ณด ์ ์ฅ | |
| function saveLikesToStorage() { | |
| if (!state.username) return; | |
| const key = getLikesStorageKey(state.username); | |
| localStorage.setItem(key, JSON.stringify(state.likedURLs)); | |
| } | |
| // ์ข์์ ํต๊ณ ์ ๋ฐ์ดํธ | |
| function updateLikeStats() { | |
| const totalCount = state.allURLs.length; | |
| const likedCount = Object.keys(state.likedURLs).length; | |
| elements.totalCount.textContent = totalCount; | |
| elements.likedCount.textContent = likedCount; | |
| } | |
| // ๋ก๋ฉ ์ํ ํ์ ํจ์ | |
| 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(); | |
| } | |
| // ์ธ์ ์ํ ํ์ธ | |
| 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.statsBar.style.display = 'flex'; | |
| // ๋ก์ปฌ ์คํ ๋ฆฌ์ง์์ ์ข์์ ์ ๋ณด ๋ก๋ | |
| state.likedURLs = loadLikesFromStorage(); | |
| // 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; | |
| // ๋ก์ปฌ ์คํ ๋ฆฌ์ง์์ ์ข์์ ์ ๋ณด ๋ก๋ | |
| state.likedURLs = loadLikesFromStorage(); | |
| elements.currentUser.textContent = state.username; | |
| elements.loginSection.style.display = 'none'; | |
| elements.loggedInSection.style.display = 'block'; | |
| elements.statsBar.style.display = 'flex'; | |
| 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.likedURLs = {}; | |
| state.allURLs = []; | |
| elements.currentUser.textContent = '๋ก๊ทธ์ธ๋์ง ์์'; | |
| elements.tokenInput.value = ''; | |
| elements.loginSection.style.display = 'block'; | |
| elements.loggedInSection.style.display = 'none'; | |
| elements.statsBar.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); | |
| 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 } = item; | |
| // ์ข์์ ํํฐ๋ง (์ข์์๋ง ๋ณด๊ธฐ ๋ชจ๋) | |
| if (state.viewMode === 'liked' && !state.likedURLs[url]) { | |
| return false; | |
| } | |
| // ๊ฒ์ ํํฐ๋ง | |
| if (searchText && !url.toLowerCase().includes(searchText) && !title.toLowerCase().includes(searchText)) { | |
| return false; | |
| } | |
| return true; | |
| }); | |
| renderCards(filteredUrls); | |
| } | |
| // ์ข์์ ํ ๊ธ | |
| function toggleLike(url, card) { | |
| if (!state.username) { | |
| showMessage('์ข์์๋ฅผ ํ๋ ค๋ฉด ํ๊น ํ์ด์ค API ํ ํฐ์ผ๋ก ์ธ์ฆ์ด ํ์ํฉ๋๋ค.', true); | |
| return; | |
| } | |
| setLoading(true); | |
| try { | |
| // ํ์ฌ ์ข์์ ์ํ ํ์ธ | |
| const isCurrentlyLiked = state.likedURLs[url] || false; | |
| // ์ํ ํ ๊ธ | |
| if (isCurrentlyLiked) { | |
| delete state.likedURLs[url]; | |
| card.classList.remove('liked'); | |
| const likeBtn = card.querySelector('.like-button'); | |
| if (likeBtn) { | |
| likeBtn.classList.remove('liked'); | |
| likeBtn.classList.add('not-liked'); | |
| } | |
| showMessage('์ข์์๋ฅผ ์ทจ์ํ์ต๋๋ค.'); | |
| } else { | |
| state.likedURLs[url] = true; | |
| card.classList.add('liked'); | |
| const likeBtn = card.querySelector('.like-button'); | |
| if (likeBtn) { | |
| likeBtn.classList.add('liked'); | |
| likeBtn.classList.remove('not-liked'); | |
| } | |
| showMessage('์ข์์๋ฅผ ์ถ๊ฐํ์ต๋๋ค.'); | |
| } | |
| // ๋ก์ปฌ ์คํ ๋ฆฌ์ง์ ์ ์ฅ | |
| saveLikesToStorage(); | |
| // ์ข์์ ํต๊ณ ์ ๋ฐ์ดํธ | |
| updateLikeStats(); | |
| // ์ข์์๋ง ๋ณด๊ธฐ ๋ชจ๋์ธ ๊ฒฝ์ฐ ๋ชฉ๋ก ๋ค์ ๋ ๋๋ง | |
| if (state.viewMode === 'liked') { | |
| filterAndRenderCards(); | |
| } | |
| } 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 } = item; | |
| const isLiked = state.likedURLs[url] || false; | |
| // ์นด๋ ์์ฑ | |
| const card = document.createElement('div'); | |
| card.className = `card ${isLiked ? 'liked' : ''}`; | |
| // ์ข์์ ๋ผ๋ฒจ | |
| const likeLabel = document.createElement('span'); | |
| likeLabel.className = 'like-label'; | |
| likeLabel.textContent = '์ข์์'; | |
| card.appendChild(likeLabel); | |
| // ์ ๋ชฉ | |
| const titleEl = document.createElement('h3'); | |
| titleEl.className = 'card-title'; | |
| titleEl.textContent = title; | |
| card.appendChild(titleEl); | |
| // 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 ${isLiked ? 'liked' : 'not-liked'}`; | |
| likeBtn.textContent = 'โฅ'; | |
| likeBtn.title = isLiked ? '์ข์์ ์ทจ์' : '์ข์์'; | |
| likeBtn.addEventListener('click', function(e) { | |
| e.preventDefault(); | |
| toggleLike(url, card); | |
| }); | |
| card.appendChild(likeBtn); | |
| // ์นด๋ ์ถ๊ฐ | |
| elements.cardsContainer.appendChild(card); | |
| }); | |
| } | |
| // ๋ชจ๋ ๋ณ๊ฒฝ ํจ์ | |
| function changeViewMode(mode) { | |
| state.viewMode = mode; | |
| // ๋ฒํผ ์ํ ์ ๋ฐ์ดํธ | |
| elements.allViewBtn.classList.toggle('active', mode === 'all'); | |
| elements.likedViewBtn.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.allViewBtn.addEventListener('click', () => changeViewMode('all')); | |
| elements.likedViewBtn.addEventListener('click', () => changeViewMode('liked')); | |
| // ์ด๊ธฐํ | |
| checkSessionStatus(); | |
| </script> | |
| </body> | |
| </html> | |
| ''') | |
| # ํ๊น ํ์ด์ค ์คํ์ด์ค์์๋ 7860 ํฌํธ ์ฌ์ฉ | |
| app.run(host='0.0.0.0', port=7860) |