초기화
This commit is contained in:
198
frontend/app.js
198
frontend/app.js
@@ -1,198 +0,0 @@
|
||||
const statusText = document.getElementById('status-text');
|
||||
const wsIndicator = document.getElementById('ws-indicator');
|
||||
const searchBtn = document.getElementById('search-btn');
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const resultsContainer = document.getElementById('discovery-results');
|
||||
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
|
||||
const dlBtn = document.getElementById('dl-btn');
|
||||
const dlUrl = document.getElementById('dl-url');
|
||||
const dlStart = document.getElementById('dl-start');
|
||||
const dlEnd = document.getElementById('dl-end');
|
||||
const confirmModal = document.getElementById('confirm-modal');
|
||||
const dlConfirmBtn = document.getElementById('dl-confirm-btn');
|
||||
const dlCancelBtn = document.getElementById('dl-cancel-btn');
|
||||
|
||||
|
||||
// --- WebSocket ---
|
||||
let ws;
|
||||
function connectWS() {
|
||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = `${proto}://${window.location.host}/ws`;
|
||||
// For local dev without docker, port might be 3000
|
||||
const finalWsUrl = window.location.port === '' ? `${proto}://${window.location.hostname}:3000/ws` : wsUrl;
|
||||
|
||||
ws = new WebSocket(finalWsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
wsIndicator.className = 'h-2 w-2 rounded-full bg-green-500';
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
statusText.textContent = event.data;
|
||||
// flash text
|
||||
statusText.classList.add('text-blue-400');
|
||||
setTimeout(() => statusText.classList.remove('text-blue-400'), 500);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
wsIndicator.className = 'h-2 w-2 rounded-full bg-red-500';
|
||||
setTimeout(connectWS, 2000); // Reconnect
|
||||
};
|
||||
}
|
||||
connectWS();
|
||||
|
||||
// --- Zone A: Search ---
|
||||
searchBtn.addEventListener('click', async () => {
|
||||
const query = searchInput.value.trim();
|
||||
if (!query) return;
|
||||
|
||||
searchBtn.disabled = true;
|
||||
searchBtn.textContent = '검색 중...';
|
||||
resultsContainer.innerHTML = '<div class="col-span-full h-full flex items-center justify-center text-gray-500 animate-pulse">인공지능이 이미지를 탐색하고 선별 중입니다...</div>';
|
||||
|
||||
try {
|
||||
const port = window.location.port ? `:${window.location.port}` : ':3000';
|
||||
const res = await fetch(`http://${window.location.hostname}${port}/api/search?q=${encodeURIComponent(query)}`);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
resultsContainer.innerHTML = `<div class="col-span-full text-red-500 p-4">Error: ${data.error}</div>`;
|
||||
} else if (data.recommended && data.recommended.length > 0) {
|
||||
resultsContainer.innerHTML = '';
|
||||
data.recommended.forEach((item, index) => {
|
||||
const delay = index * 100;
|
||||
resultsContainer.innerHTML += `
|
||||
<div class="recommend-card bg-[#252525] rounded-lg overflow-hidden border border-purple-500/30 hover:border-purple-500 transition-colors cursor-pointer group relative" style="animation-delay: ${delay}ms; opacity:0;">
|
||||
<span class="absolute top-2 left-2 bg-purple-600/90 text-xs px-2 py-1 rounded text-white font-medium backdrop-blur-sm z-10 shadow-lg border border-purple-400/50">✨ AI Recommended</span>
|
||||
<div class="h-32 w-full bg-black overflow-hidden relative">
|
||||
<img src="${item.url}" class="w-full h-full object-cover opacity-80 group-hover:opacity-100 group-hover:scale-105 transition-all duration-300" onerror="this.src='https://via.placeholder.com/300x200?text=Image+Load+Error'" />
|
||||
</div>
|
||||
<div class="p-3">
|
||||
<p class="text-xs text-gray-300 line-clamp-3 leading-relaxed">${item.reason}</p>
|
||||
</div>
|
||||
<a href="${item.url}" target="_blank" class="absolute inset-0"></a>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
resultsContainer.innerHTML = `<div class="col-span-full text-gray-500 p-4">결과를 찾을 수 없습니다.</div>`;
|
||||
}
|
||||
} catch (err) {
|
||||
resultsContainer.innerHTML = `<div class="col-span-full text-red-500 p-4">Exception: ${err.message}</div>`;
|
||||
} finally {
|
||||
searchBtn.disabled = false;
|
||||
searchBtn.textContent = '검색';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// --- Zone B: Upload ---
|
||||
dropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('border-purple-500', 'bg-[#303030]');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragleave', () => {
|
||||
dropzone.classList.remove('border-purple-500', 'bg-[#303030]');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('border-purple-500', 'bg-[#303030]');
|
||||
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
uploadFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
dropzone.addEventListener('click', () => fileInput.click());
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
uploadFile(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
statusText.textContent = `Uploading ${file.name}...`;
|
||||
|
||||
try {
|
||||
const port = window.location.port ? `:${window.location.port}` : ':3000';
|
||||
const res = await fetch(`http://${window.location.hostname}${port}/api/upload`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.error) {
|
||||
statusText.textContent = `Upload Error: ${data.error}`;
|
||||
} else {
|
||||
statusText.textContent = `Upload Success: ${data.filename}`;
|
||||
}
|
||||
} catch (err) {
|
||||
statusText.textContent = `Upload Failed: ${err.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- Zone C: Download ---
|
||||
async function requestDownload(confirm = false) {
|
||||
const url = dlUrl.value.trim();
|
||||
if (!url) return;
|
||||
|
||||
dlBtn.disabled = true;
|
||||
dlBtn.textContent = '요청 중...';
|
||||
|
||||
const payload = {
|
||||
url: url,
|
||||
start: dlStart.value.trim(),
|
||||
end: dlEnd.value.trim()
|
||||
};
|
||||
|
||||
try {
|
||||
const port = window.location.port ? `:${window.location.port}` : ':3000';
|
||||
const res = await fetch(`http://${window.location.hostname}${port}/api/download${confirm ? '?confirm=true' : ''}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (res.status === 409) {
|
||||
// Duplicate!
|
||||
confirmModal.classList.remove('hidden');
|
||||
confirmModal.classList.add('flex');
|
||||
dlBtn.disabled = false;
|
||||
dlBtn.textContent = '다운로드 시작';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (data.error) {
|
||||
statusText.textContent = `Download Error: ${data.error}`;
|
||||
} else {
|
||||
// The WS will handle the progress
|
||||
dlUrl.value = '';
|
||||
dlStart.value = '';
|
||||
dlEnd.value = '';
|
||||
confirmModal.classList.add('hidden');
|
||||
confirmModal.classList.remove('flex');
|
||||
}
|
||||
} catch (err) {
|
||||
statusText.textContent = `Download Request Failed: ${err.message}`;
|
||||
} finally {
|
||||
dlBtn.disabled = false;
|
||||
dlBtn.textContent = '다운로드 시작';
|
||||
}
|
||||
}
|
||||
|
||||
dlBtn.addEventListener('click', () => requestDownload(false));
|
||||
dlConfirmBtn.addEventListener('click', () => requestDownload(true));
|
||||
dlCancelBtn.addEventListener('click', () => {
|
||||
confirmModal.classList.add('hidden');
|
||||
confirmModal.classList.remove('flex');
|
||||
});
|
||||
Reference in New Issue
Block a user