feat: 제품 이미지 수동 크롤링 - MSSQL 검색 인터페이스 추가

- OTC 라벨처럼 제품명 검색 → 선택 → 크롤링
- 바코드 직접 입력 불필요
- MSSQL 검색 API 재사용
This commit is contained in:
thug0bin 2026-03-02 23:37:31 +09:00
parent 29648e3a7d
commit ee28f97c11

View File

@ -328,6 +328,9 @@
<button class="btn btn-primary" onclick="crawlToday()">
🔄 오늘 판매 제품 크롤링
</button>
<button class="btn btn-secondary" onclick="openManualCrawl()">
수동 크롤링
</button>
<a href="/admin" class="btn btn-secondary">← 어드민</a>
</div>
</header>
@ -370,6 +373,43 @@
</div>
</div>
<!-- 수동 크롤링 모달 -->
<div class="modal" id="manualCrawlModal">
<div class="modal-content" style="max-width: 600px;">
<h3> 제품 검색 & 크롤링</h3>
<p style="color: #9ca3af; margin-bottom: 16px; font-size: 13px;">
약국 DB에서 제품을 검색하고, 선택하면 yakkok.com에서 이미지를 가져옵니다
</p>
<!-- 검색 영역 -->
<div style="display: flex; gap: 8px; margin-bottom: 16px;">
<input type="text" id="mssqlSearchInput" class="search-box" style="flex: 1;"
placeholder="제품명 검색 (예: 타이레놀, 센스큐탐...)"
onkeypress="if(event.key==='Enter') searchMssqlProducts()">
<button class="btn btn-primary" onclick="searchMssqlProducts()">검색</button>
</div>
<!-- 검색 결과 -->
<div id="mssqlSearchResults" style="max-height: 300px; overflow-y: auto; margin-bottom: 16px;">
<div style="color: #6b7280; text-align: center; padding: 20px;">
제품명을 검색하세요
</div>
</div>
<!-- 선택된 제품 -->
<div id="selectedProduct" style="display: none; background: rgba(139,92,246,0.1); border: 1px solid rgba(139,92,246,0.3); border-radius: 8px; padding: 12px; margin-bottom: 16px;">
<div style="font-size: 12px; color: #9ca3af; margin-bottom: 4px;">선택된 제품</div>
<div id="selectedProductName" style="font-weight: 600;"></div>
<div id="selectedProductBarcode" style="font-size: 12px; color: #a855f7; font-family: monospace;"></div>
</div>
<div style="text-align: right;">
<button class="btn btn-secondary" onclick="closeManualCrawl()">취소</button>
<button class="btn btn-primary" id="crawlSelectedBtn" onclick="crawlSelected()" disabled>🔄 크롤링 시작</button>
</div>
</div>
</div>
<!-- 상세 모달 -->
<div class="modal" id="detailModal">
<div class="modal-content">
@ -561,15 +601,133 @@
setTimeout(() => toast.remove(), 4000);
}
// 수동 크롤링
let selectedProductData = null;
function openManualCrawl() {
selectedProductData = null;
document.getElementById('mssqlSearchInput').value = '';
document.getElementById('mssqlSearchResults').innerHTML = '<div style="color: #6b7280; text-align: center; padding: 20px;">제품명을 검색하세요</div>';
document.getElementById('selectedProduct').style.display = 'none';
document.getElementById('crawlSelectedBtn').disabled = true;
document.getElementById('manualCrawlModal').classList.add('show');
document.getElementById('mssqlSearchInput').focus();
}
function closeManualCrawl() {
document.getElementById('manualCrawlModal').classList.remove('show');
}
async function searchMssqlProducts() {
const query = document.getElementById('mssqlSearchInput').value.trim();
if (!query) {
showToast('검색어를 입력하세요', 'error');
return;
}
const resultsDiv = document.getElementById('mssqlSearchResults');
resultsDiv.innerHTML = '<div style="color: #9ca3af; text-align: center; padding: 20px;">검색 중...</div>';
try {
// OTC 라벨 검색 API 재사용
const res = await fetch(`/api/admin/otc-labels/search-mssql?q=${encodeURIComponent(query)}`);
const data = await res.json();
if (data.success && data.drugs.length > 0) {
resultsDiv.innerHTML = data.drugs.map(drug => `
<div class="search-result-item" onclick="selectProduct('${drug.barcode}', '${drug.drug_code}', '${escapeHtml(drug.goods_name)}')"
style="padding: 12px; border-bottom: 1px solid rgba(255,255,255,0.1); cursor: pointer; transition: background 0.2s;"
onmouseover="this.style.background='rgba(139,92,246,0.1)'"
onmouseout="this.style.background='transparent'">
<div style="font-weight: 500;">${drug.goods_name}</div>
<div style="font-size: 12px; color: #9ca3af;">
<span style="color: #a855f7; font-family: monospace;">${drug.barcode || '바코드없음'}</span>
${drug.sale_price ? ` · ₩${Math.floor(drug.sale_price).toLocaleString()}` : ''}
</div>
</div>
`).join('');
} else {
resultsDiv.innerHTML = '<div style="color: #6b7280; text-align: center; padding: 20px;">검색 결과가 없습니다</div>';
}
} catch (err) {
resultsDiv.innerHTML = `<div style="color: #ef4444; text-align: center; padding: 20px;">오류: ${err.message}</div>`;
}
}
function selectProduct(barcode, drugCode, productName) {
if (!barcode) {
showToast('바코드가 없는 제품은 크롤링할 수 없습니다', 'error');
return;
}
selectedProductData = { barcode, drugCode, productName };
document.getElementById('selectedProductName').textContent = productName;
document.getElementById('selectedProductBarcode').textContent = barcode;
document.getElementById('selectedProduct').style.display = 'block';
document.getElementById('crawlSelectedBtn').disabled = false;
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/'/g, "\\'").replace(/"/g, '&quot;');
}
async function crawlSelected() {
if (!selectedProductData) {
showToast('제품을 선택하세요', 'error');
return;
}
const { barcode, drugCode, productName } = selectedProductData;
closeManualCrawl();
showToast(`"${productName}" 크롤링 시작...`, 'info');
try {
const res = await fetch('/api/admin/product-images/crawl', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
products: [[barcode, drugCode || null, productName]]
})
});
const data = await res.json();
if (data.success) {
const r = data.result;
if (r.success > 0) {
showToast(`✅ "${productName}" 이미지 수집 성공!`, 'success');
} else if (r.skipped > 0) {
showToast(`이미 등록된 바코드입니다`, 'info');
} else {
showToast(`❌ 이미지를 찾지 못했습니다`, 'error');
}
loadStats();
loadImages();
} else {
showToast(data.error || '크롤링 실패', 'error');
}
} catch (err) {
showToast('오류: ' + err.message, 'error');
}
}
// ESC로 모달 닫기
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeModal();
if (e.key === 'Escape') {
closeModal();
closeManualCrawl();
}
});
// 모달 외부 클릭으로 닫기
document.getElementById('detailModal').addEventListener('click', (e) => {
if (e.target.id === 'detailModal') closeModal();
});
document.getElementById('manualCrawlModal').addEventListener('click', (e) => {
if (e.target.id === 'manualCrawlModal') closeManualCrawl();
});
</script>
</body>
</html>