feat: 제품 이미지 수동 크롤링 - MSSQL 검색 인터페이스 추가
- OTC 라벨처럼 제품명 검색 → 선택 → 크롤링 - 바코드 직접 입력 불필요 - MSSQL 검색 API 재사용
This commit is contained in:
parent
29648e3a7d
commit
ee28f97c11
@ -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, '"');
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user