seawolf2357's picture
Create templates/index.html
1bba99b verified
# app.py์˜ __main__ ๋ถ€๋ถ„ ์ˆ˜์ • (index.html ํ…œํ”Œ๋ฆฟ ์ƒ์„ฑ ๋ถ€๋ถ„)
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: 1600px;
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;
}
/* ๊ทธ๋ฆฌ๋“œ ๋ ˆ์ด์•„์›ƒ ์Šคํƒ€์ผ */
.grid-container {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.grid-item {
border: 1px solid #ddd;
border-radius: 8px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
.grid-item:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.grid-item.liked {
border-color: #ff4757;
background-color: #ffebee;
}
.grid-header {
padding: 0.5rem 1rem;
border-bottom: 1px solid #eee;
position: relative;
}
.grid-title {
font-size: 1rem;
margin: 0;
padding-right: 30px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.grid-content {
flex: 1;
position: relative;
height: 300px;
}
.iframe-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.iframe-container iframe {
width: 100%;
height: 100%;
border: none;
}
.like-button {
position: absolute;
top: 0.5rem;
right: 0.5rem;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: none;
background: transparent;
font-size: 1.2rem;
cursor: pointer;
transition: all 0.3s ease;
color: #ddd;
padding: 0;
}
.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;
z-index: 10;
}
.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;
}
.view-toggle {
margin-top: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.view-toggle button {
margin-left: 0.5rem;
}
/* ๋ฐ˜์‘ํ˜• ์„ค์ • */
@media (max-width: 1400px) {
.grid-container {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 1024px) {
.grid-container {
grid-template-columns: repeat(2, 1fr);
}
}
@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;
}
.grid-container {
grid-template-columns: 1fr;
}
}
</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 class="view-toggle">
<div>
<input type="checkbox" id="embedToggle" checked />
<label for="embedToggle">URL ์ž„๋ฒ ๋”ฉ ๋ณด๊ธฐ</label>
</div>
</div>
<div id="statusMessage" class="status-message"></div>
<div id="loadingIndicator" class="loading">
<div class="spinner"></div>
</div>
<div id="gridContainer" class="grid-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'),
gridContainer: document.getElementById('gridContainer'),
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'),
embedToggle: document.getElementById('embedToggle')
};
// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ƒํƒœ
const state = {
username: null,
allURLs: [],
isLoading: false,
viewMode: 'all', // 'all' ๋˜๋Š” 'liked'
embedView: true // iframe ์ž„๋ฒ ๋”ฉ ์—ฌ๋ถ€
};
// ๋กœ๋”ฉ ์ƒํƒœ ํ‘œ์‹œ ํ•จ์ˆ˜
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.gridContainer.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();
renderGrid();
} 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();
renderGrid();
}
showMessage(data.message);
} else {
showMessage(data.message || '์ข‹์•„์š” ์ƒํƒœ ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.', true);
}
} catch (error) {
console.error('์ข‹์•„์š” ํ† ๊ธ€ ์˜ค๋ฅ˜:', error);
showMessage(`์ข‹์•„์š” ํ† ๊ธ€ ์˜ค๋ฅ˜: ${error.message}`, true);
}
}
// ๊ทธ๋ฆฌ๋“œ ๋ Œ๋”๋ง
function renderGrid() {
elements.gridContainer.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.gridContainer.appendChild(emptyMessage);
return;
}
// ๊ทธ๋ฆฌ๋“œ ์•„์ดํ…œ ์ƒ์„ฑ
urlsToShow.forEach(item => {
const gridItem = document.createElement('div');
gridItem.className = `grid-item ${item.is_liked ? 'liked' : ''}`;
if (item.is_liked) {
const badge = document.createElement('div');
badge.className = 'like-badge';
badge.textContent = '์ข‹์•„์š”';
gridItem.appendChild(badge);
}
// ํ—ค๋” ๋ถ€๋ถ„
const header = document.createElement('div');
header.className = 'grid-header';
const title = document.createElement('h3');
title.className = 'grid-title';
title.textContent = item.title;
const likeButton = document.createElement('button');
likeButton.className = `like-button ${item.is_liked ? 'liked' : ''}`;
likeButton.innerHTML = 'โค';
likeButton.dataset.url = item.url;
likeButton.addEventListener('click', (e) => {
e.preventDefault();
toggleLike(item.url);
});
header.appendChild(title);
header.appendChild(likeButton);
// ์ปจํ…์ธ  ๋ถ€๋ถ„ (iframe ์ž„๋ฒ ๋”ฉ)
const content = document.createElement('div');
content.className = 'grid-content';
if (state.embedView) {
const iframeContainer = document.createElement('div');
iframeContainer.className = 'iframe-container';
const iframe = document.createElement('iframe');
iframe.src = item.url;
iframe.title = item.title;
iframe.sandbox = 'allow-scripts allow-same-origin allow-forms';
iframe.loading = 'lazy';
iframeContainer.appendChild(iframe);
content.appendChild(iframeContainer);
} else {
// ์ž„๋ฒ ๋”ฉ ์—†๋Š” ๊ฒฝ์šฐ ๋งํฌ๋งŒ ํ‘œ์‹œ
const linkContainer = document.createElement('div');
linkContainer.style.padding = '1rem';
const link = document.createElement('a');
link.href = item.url;
link.textContent = item.url;
link.target = '_blank';
const owner = document.createElement('div');
owner.textContent = `์†Œ์œ ์ž: ${item.model_info.owner}`;
owner.style.marginTop = '0.5rem';
const repo = document.createElement('div');
repo.textContent = `์ €์žฅ์†Œ: ${item.model_info.repo}`;
const type = document.createElement('div');
type.textContent = `์œ ํ˜•: ${item.model_info.type}`;
linkContainer.appendChild(link);
linkContainer.appendChild(owner);
linkContainer.appendChild(repo);
linkContainer.appendChild(type);
content.appendChild(linkContainer);
}
// ๊ทธ๋ฆฌ๋“œ ์•„์ดํ…œ์— ์ถ”๊ฐ€
gridItem.appendChild(header);
gridItem.appendChild(content);
elements.gridContainer.appendChild(gridItem);
});
}
// ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ๋“ฑ๋ก
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', renderGrid);
// ํ•„ํ„ฐ ๋ฒ„ํŠผ - ์ „์ฒด ๋ณด๊ธฐ
elements.allUrlsBtn.addEventListener('click', () => {
elements.allUrlsBtn.classList.add('active');
elements.likedUrlsBtn.classList.remove('active');
state.viewMode = 'all';
renderGrid();
});
// ํ•„ํ„ฐ ๋ฒ„ํŠผ - ์ข‹์•„์š”๋งŒ ๋ณด๊ธฐ
elements.likedUrlsBtn.addEventListener('click', () => {
elements.likedUrlsBtn.classList.add('active');
elements.allUrlsBtn.classList.remove('active');
state.viewMode = 'liked';
renderGrid();
});
// ์ž„๋ฒ ๋”ฉ ํ† ๊ธ€
elements.embedToggle.addEventListener('change', () => {
state.embedView = elements.embedToggle.checked;
renderGrid();
});
}
// ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
function init() {
registerEventListeners();
checkSessionStatus();
}
// ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ดˆ๊ธฐํ™”
init();
</script>
</body>
</html>
''')
app.run(debug=True, host='0.0.0.0', port=5000)