good / index.html
cutechicken's picture
Update index.html
eb62174 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>Hugging Face URL μΉ΄λ“œ 리슀트</title>
<style>
/* μΉ΄λ“œλ“€μ„ 담을 μ»¨ν…Œμ΄λ„ˆ */
.container {
display: flex;
flex-wrap: wrap;
gap: 1rem;
padding: 1rem;
}
/* 각 μΉ΄λ“œ μŠ€νƒ€μΌ */
.card {
border: 1px solid #ccc;
border-radius: 5px;
padding: 1rem;
width: 300px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
position: relative;
background-color: #f9f9f9;
}
/* μΉ΄λ“œ λ‚΄λΆ€ 링크 μŠ€νƒ€μΌ */
.card a {
text-decoration: none;
color: #333;
word-break: break-all;
}
/* μ’‹μ•„μš” λ²„νŠΌ κΈ°λ³Έ μŠ€νƒ€μΌ */
.like-button {
position: absolute;
top: 1rem;
right: 1rem;
border: none;
background: transparent;
font-size: 1.5rem;
cursor: pointer;
transition: color 0.2s;
}
/* μ’‹μ•„μš” ν•œ μƒνƒœ: 빨간색 */
.like-button.liked {
color: red;
}
/* μ’‹μ•„μš” μ•ˆν•œ μƒνƒœ: 흰색 (ν…Œλ‘λ¦¬ 효과) */
.like-button.not-liked {
color: white;
-webkit-text-stroke: 1px #333;
}
/* 토큰 μž…λ ₯ 및 μƒνƒœ ν‘œμ‹œ μ˜μ—­ */
.auth-controls {
padding: 1rem;
margin-bottom: 1rem;
background-color: #f0f0f0;
border-radius: 5px;
display: flex;
justify-content: space-between;
align-items: center;
}
.auth-controls input {
padding: 0.5rem;
width: 300px;
border: 1px solid #ccc;
border-radius: 4px;
}
.auth-controls button {
padding: 0.5rem 1rem;
margin-left: 0.5rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.auth-controls button:hover {
background-color: #45a049;
}
/* λ‘œλ”© ν‘œμ‹œ */
.loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
font-size: 1.5rem;
}
/* μƒνƒœ λ©”μ‹œμ§€ */
.status-message {
margin: 1rem;
padding: 1rem;
border-radius: 4px;
}
.success {
background-color: #dff0d8;
color: #3c763d;
}
.error {
background-color: #f2dede;
color: #a94442;
}
</style>
</head>
<body>
<!-- 토큰 μž…λ ₯ 및 인증 컨트둀 -->
<div class="auth-controls" id="authControls">
<div>
<span>Hugging Face 인증: </span>
<span id="authStatus">μΈμ¦λ˜μ§€ μ•ŠμŒ</span>
</div>
<div>
<input type="password" id="tokenInput" placeholder="Hugging Face API 토큰 μž…λ ₯" />
<button id="authButton">μΈμ¦ν•˜κΈ°</button>
<button id="logoutButton" style="display: none; background-color: #f44336;">λ‘œκ·Έμ•„μ›ƒ</button>
</div>
</div>
<!-- μƒνƒœ λ©”μ‹œμ§€ -->
<div id="statusMessage" class="status-message" style="display: none;"></div>
<!-- λ‘œλ”© ν‘œμ‹œ -->
<div class="loading" id="loadingIndicator" style="display: none;">
데이터λ₯Ό λΆˆλŸ¬μ˜€λŠ” 쀑...
</div>
<!-- μΉ΄λ“œ μ»¨ν…Œμ΄λ„ˆ -->
<div class="container" id="cardsContainer"></div>
<script>
// API μ—”λ“œν¬μΈνŠΈ
const HF_API_BASE = 'https://huggingface.co';
// μƒνƒœ 관리
const state = {
token: null,
username: null,
likedModels: {},
isLoading: false
};
// DOM μš”μ†Œ μ°Έμ‘°
const elements = {
tokenInput: document.getElementById('tokenInput'),
authButton: document.getElementById('authButton'),
logoutButton: document.getElementById('logoutButton'),
authStatus: document.getElementById('authStatus'),
cardsContainer: document.getElementById('cardsContainer'),
loadingIndicator: document.getElementById('loadingIndicator'),
statusMessage: document.getElementById('statusMessage')
};
// Hugging Face의 spaces/models URL λͺ©λ‘
const 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",
// λ‚˜λ¨Έμ§€ URL λͺ©λ‘... μ‹€μ œλ‘œλŠ” λͺ¨λ“  URL을 포함해야 함
];
// URLμ—μ„œ λͺ¨λΈ/슀페이슀 ID μΆ”μΆœ
function extractModelInfo(url) {
try {
const parts = url.split('/');
let type, owner, repo;
// URL ν˜•μ‹μ— 따라 λ‹€λ₯Έ 처리
if (parts[3] === 'spaces' || parts[3] === 'models') {
type = parts[3];
owner = parts[4];
repo = parts[5];
} else {
// ν˜•μ‹μ΄ λ‹€λ₯Έ 경우 (예: https://huggingface.co/deepseek/deepseek-ai)
type = 'models'; // κΈ°λ³Έκ°’μœΌλ‘œ models μ‚¬μš©
owner = parts[3];
repo = parts[4];
}
// μ—†λŠ” 값이 있으면 κΈ°λ³Έκ°’ μ„€μ •
type = type || 'models';
owner = owner || '';
repo = repo || '';
return { type, owner, repo, fullId: `${owner}/${repo}` };
} catch (e) {
console.error('URL νŒŒμ‹± 였λ₯˜:', e, url);
return { type: 'models', owner: '', repo: '', fullId: '' };
}
}
// URL의 λ§ˆμ§€λ§‰ 뢀뢄을 제λͺ©μœΌλ‘œ μΆ”μΆœ (언더바, ν•˜μ΄ν”ˆμ„ 곡백으둜 λ³€ν™˜)
function extractTitle(url) {
const parts = url.split("/");
let title = parts[parts.length - 1];
return title.replace(/[_-]/g, " ");
}
// λ‘œλ”© μƒνƒœ ν‘œμ‹œ ν•¨μˆ˜
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);
}
// Hugging Face API 호좜 ν•¨μˆ˜
async function fetchWithToken(endpoint, options = {}) {
if (!state.token) {
throw new Error('인증 토큰이 μ—†μŠ΅λ‹ˆλ‹€.');
}
const url = `${HF_API_BASE}${endpoint}`;
console.log('API 호좜:', url, options.method || 'GET');
const headers = {
'Authorization': `Bearer ${state.token}`,
'Content-Type': 'application/json',
...options.headers
};
try {
const response = await fetch(url, {
...options,
headers
});
// 디버깅을 μœ„ν•œ 응닡 정보 좜λ ₯
console.log('API 응닡 μƒνƒœ:', response.status, response.statusText);
return response;
} catch (error) {
console.error('API 호좜 였λ₯˜:', error);
throw error;
}
}
// μ‚¬μš©μž 정보 κ°€μ Έμ˜€κΈ°
async function fetchUserInfo() {
const response = await fetchWithToken('/api/whoami-v2');
if (!response.ok) {
throw new Error('μ‚¬μš©μž 정보λ₯Ό κ°€μ Έμ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μƒνƒœ μ½”λ“œ: ' + response.status);
}
return response.json();
}
// μ‚¬μš©μžκ°€ μ’‹μ•„μš”ν•œ λͺ¨λΈ/슀페이슀 λͺ©λ‘ κ°€μ Έμ˜€κΈ°
async function fetchLikedRepos() {
try {
// μ’‹μ•„μš”ν•œ λͺ¨λΈ κ°€μ Έμ˜€κΈ°
const modelsResponse = await fetchWithToken('/api/me/likes');
if (!modelsResponse.ok) {
console.error('μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ° 응닡:', modelsResponse.status, modelsResponse.statusText);
throw new Error('μ’‹μ•„μš” λͺ©λ‘μ„ κ°€μ Έμ˜€λŠ”λ° μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€. μƒνƒœ μ½”λ“œ: ' + modelsResponse.status);
}
const likedModels = await modelsResponse.json();
console.log('κ°€μ Έμ˜¨ μ’‹μ•„μš” λͺ©λ‘:', likedModels);
// κ²°κ³Όλ₯Ό 객체둜 λ³€ν™˜ (λΉ λ₯Έ 검색을 μœ„ν•΄)
const likedMap = {};
likedModels.forEach(model => {
likedMap[`${model.owner}/${model.name}`] = true;
});
return likedMap;
} catch (error) {
console.error('μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ° 였λ₯˜:', error);
throw error;
}
}
// μ’‹μ•„μš” ν† κΈ€ API 호좜
async function toggleLikeAPI(type, owner, repo, isLiked) {
try {
const method = isLiked ? 'DELETE' : 'POST';
const response = await fetchWithToken(`/api/${type}/${owner}/${repo}/like`, {
method
});
if (!response.ok) {
console.error('μ’‹μ•„μš” ν† κΈ€ 응닡:', response.status, response.statusText);
throw new Error(`μ’‹μ•„μš” ${isLiked ? 'μ·¨μ†Œ' : 'μΆ”κ°€'} μ‹€νŒ¨. μƒνƒœ μ½”λ“œ: ${response.status}`);
}
return response.ok;
} catch (error) {
console.error('μ’‹μ•„μš” ν† κΈ€ API 였λ₯˜:', error);
throw error;
}
}
// 인증 처리
async function authenticate(token) {
if (!token.trim()) {
showMessage('토큰을 μž…λ ₯ν•΄μ£Όμ„Έμš”.', true);
return;
}
setLoading(true);
try {
// 토큰 μ €μž₯
state.token = token;
console.log('인증 μ‹œλ„ (토큰 일뢀):', token.substring(0, 4) + '...');
// μ‚¬μš©μž 정보 κ°€μ Έμ˜€κΈ°
const userInfo = await fetchUserInfo();
console.log('μ‚¬μš©μž 정보:', userInfo);
// μ‚¬μš©μž 이름 μΆ”μΆœ (API 응닡 ꡬ쑰에 따라 λ‹€λ₯Ό 수 있음)
if (userInfo.name) {
state.username = userInfo.name;
} else if (userInfo.user && userInfo.user.username) {
state.username = userInfo.user.username;
} else if (userInfo.username) {
state.username = userInfo.username;
} else {
state.username = '인증된 μ‚¬μš©μž';
}
// μ‹€μ œ μ’‹μ•„μš” λͺ©λ‘ κ°€μ Έμ˜€κΈ°
state.likedModels = await fetchLikedRepos();
// UI μ—…λ°μ΄νŠΈ
elements.authStatus.textContent = `${state.username}λ‹˜μœΌλ‘œ 인증됨`;
elements.tokenInput.style.display = 'none';
elements.authButton.style.display = 'none';
elements.logoutButton.style.display = 'inline-block';
showMessage('인증 성곡! μ’‹μ•„μš” 정보λ₯Ό λΆˆλŸ¬μ™”μŠ΅λ‹ˆλ‹€.');
// μΉ΄λ“œ λ‹€μ‹œ λ Œλ”λ§
renderCards();
} catch (error) {
console.error('인증 였λ₯˜:', error);
showMessage(`인증에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€: ${error.message}`, true);
state.token = null;
} finally {
setLoading(false);
}
}
// λ‘œκ·Έμ•„μ›ƒ 처리
function logout() {
// μƒνƒœ μ΄ˆκΈ°ν™”
state.token = null;
state.username = null;
state.likedModels = {};
// UI μ—…λ°μ΄νŠΈ
elements.authStatus.textContent = 'μΈμ¦λ˜μ§€ μ•ŠμŒ';
elements.tokenInput.style.display = 'inline-block';
elements.tokenInput.value = '';
elements.authButton.style.display = 'inline-block';
elements.logoutButton.style.display = 'none';
showMessage('λ‘œκ·Έμ•„μ›ƒλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
// μΉ΄λ“œ λ‹€μ‹œ λ Œλ”λ§
renderCards();
}
// μ’‹μ•„μš” ν† κΈ€ 처리
async function toggleLike(url, button) {
if (!state.token) {
showMessage('μ’‹μ•„μš”λ₯Ό ν•˜λ €λ©΄ HF ν† ν°μœΌλ‘œ 인증이 ν•„μš”ν•©λ‹ˆλ‹€.', true);
return;
}
const modelInfo = extractModelInfo(url);
const modelId = modelInfo.fullId;
const isCurrentlyLiked = state.likedModels[modelId] || false;
setLoading(true);
try {
// μ‹€μ œ API 호좜둜 μ’‹μ•„μš” μƒνƒœ ν† κΈ€
await toggleLikeAPI(modelInfo.type, modelInfo.owner, modelInfo.repo, isCurrentlyLiked);
// μƒνƒœ μ—…λ°μ΄νŠΈ
if (isCurrentlyLiked) {
delete state.likedModels[modelId];
button.classList.remove("liked");
button.classList.add("not-liked");
showMessage(`${modelInfo.repo}에 λŒ€ν•œ μ’‹μ•„μš”λ₯Ό μ·¨μ†Œν–ˆμŠ΅λ‹ˆλ‹€.`);
} else {
state.likedModels[modelId] = true;
button.classList.add("liked");
button.classList.remove("not-liked");
showMessage(`${modelInfo.repo}λ₯Ό μ’‹μ•„μš” ν–ˆμŠ΅λ‹ˆλ‹€.`);
}
} catch (error) {
showMessage('μ’‹μ•„μš” 처리 쀑 였λ₯˜κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.', true);
console.error('μ’‹μ•„μš” 였λ₯˜:', error);
} finally {
setLoading(false);
}
}
// μΉ΄λ“œ λ Œλ”λ§
function renderCards() {
// μΉ΄λ“œ μ»¨ν…Œμ΄λ„ˆ μ΄ˆκΈ°ν™”
elements.cardsContainer.innerHTML = '';
// 각 URL에 λŒ€ν•΄ μΉ΄λ“œ 생성
urls.forEach(url => {
const card = document.createElement("div");
card.className = "card";
// 제λͺ©
const titleEl = document.createElement("h3");
titleEl.textContent = extractTitle(url);
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";
likeBtn.textContent = "β™₯";
// μ’‹μ•„μš” μƒνƒœ μ„€μ •
try {
const modelInfo = extractModelInfo(url);
const isLiked = state.token && state.likedModels[modelInfo.fullId];
if (isLiked) {
likeBtn.classList.add("liked");
} else {
likeBtn.classList.add("not-liked");
}
} catch (e) {
// URL νŒŒμ‹± 였λ₯˜ λ“± μ˜ˆμ™Έ 처리
likeBtn.classList.add("not-liked");
}
likeBtn.addEventListener("click", function(e) {
e.preventDefault(); // 링크 클릭 λ°©μ§€
toggleLike(url, likeBtn);
});
card.appendChild(likeBtn);
elements.cardsContainer.appendChild(card);
});
}
// 이벀트 λ¦¬μŠ€λ„ˆ μ„€μ •
elements.authButton.addEventListener('click', () => {
authenticate(elements.tokenInput.value);
});
elements.logoutButton.addEventListener('click', logout);
// μ—”ν„° ν‚€λ‘œ 인증 κ°€λŠ₯ν•˜κ²Œ
elements.tokenInput.addEventListener('keypress', (event) => {
if (event.key === 'Enter') {
authenticate(elements.tokenInput.value);
}
});
// 초기 λ Œλ”λ§
renderCards();
</script>
</body>
</html>