feat: 더블클릭 시 지오영+수인 동시 재고 조회

- openWholesaleModal: 두 도매상 동시 API 호출
- Promise.all로 병렬 조회 (빠른 응답)
- 도매상별 섹션 구분 UI (지오영: 청록, 수인: 보라)
- 각 도매상별 담기 버튼
- 수인은 단가 정보도 표시
This commit is contained in:
thug0bin 2026-03-06 11:59:13 +09:00
parent 90d993156e
commit 19c70e42fb

View File

@ -1279,14 +1279,15 @@
if (e.key === 'Enter') loadUsageData();
});
// ──────────────── 지오영 재고 조회 ────────────────
let currentGeoyoungItem = null;
// ──────────────── 도매상 재고 조회 (지오영 + 수인) ────────────────
let currentWholesaleItem = null;
window.wholesaleItems = { geoyoung: [], sooin: [] };
function openGeoyoungModal(idx) {
function openWholesaleModal(idx) {
const item = usageData[idx];
if (!item) return;
currentGeoyoungItem = item;
currentWholesaleItem = item;
// 모달 열기
document.getElementById('geoModalProductName').textContent = item.product_name;
@ -1300,142 +1301,193 @@
document.getElementById('geoResultBody').innerHTML = `
<div class="geo-loading">
<div class="loading-spinner"></div>
<div>지오영 재고 조회 중...</div>
<div>도매상 재고 조회 중... (지오영 + 수인)</div>
</div>`;
document.getElementById('geoSearchKeyword').style.display = 'none';
// API 호출 (보험코드로 먼저 시도)
searchGeoyoung(item.drug_code, item.product_name);
// 두 도매상 동시 호출
searchAllWholesalers(item.drug_code, item.product_name);
}
function closeGeoyoungModal() {
document.getElementById('geoyoungModal').classList.remove('show');
currentGeoyoungItem = null;
currentWholesaleItem = null;
}
async function searchGeoyoung(kdCode, productName) {
async function searchAllWholesalers(kdCode, productName) {
const resultBody = document.getElementById('geoResultBody');
// 두 도매상 동시 호출
const [geoResult, sooinResult] = await Promise.all([
searchGeoyoungAPI(kdCode, productName),
searchSooinAPI(kdCode)
]);
// 결과 저장
window.wholesaleItems = {
geoyoung: geoResult.items || [],
sooin: sooinResult.items || []
};
// 통합 렌더링
renderWholesaleResults(geoResult, sooinResult);
}
async function searchGeoyoungAPI(kdCode, productName) {
try {
// 1차: 보험코드(KD코드)로 검색
// 1차: 보험코드로 검색
let response = await fetch(`/api/geoyoung/stock?kd_code=${encodeURIComponent(kdCode)}`);
let data = await response.json();
// 결과 없으면 성분명으로 재검색
if (data.success && data.count === 0) {
document.getElementById('geoResultBody').innerHTML = `
<div class="geo-loading">
<div class="loading-spinner"></div>
<div>성분명으로 재검색 중...</div>
</div>`;
response = await fetch(`/api/geoyoung/stock-by-name?product_name=${encodeURIComponent(productName)}`);
data = await response.json();
}
if (!data.success) {
resultBody.innerHTML = `
<div class="geo-error">
<div>❌ ${data.message || '조회 실패'}</div>
</div>`;
return;
}
if (data.count === 0) {
resultBody.innerHTML = `
<div class="geo-empty">
<div>📭 지오영에 해당 제품이 없습니다</div>
</div>`;
return;
}
// 검색어 표시
if (data.extracted_ingredient) {
document.getElementById('geoSearchKeyword').textContent = `검색: "${data.extracted_ingredient}"`;
document.getElementById('geoSearchKeyword').style.display = 'block';
}
// 결과 렌더링
renderGeoyoungResults(data.items);
return data;
} catch (err) {
resultBody.innerHTML = `
<div class="geo-error">
<div>❌ 네트워크 오류: ${err.message}</div>
</div>`;
return { success: false, error: err.message, items: [] };
}
}
async function searchSooinAPI(kdCode) {
try {
const response = await fetch(`/api/sooin/stock?keyword=${encodeURIComponent(kdCode)}`);
const data = await response.json();
return data;
} catch (err) {
return { success: false, error: err.message, items: [] };
}
}
function renderGeoyoungResults(items) {
function renderWholesaleResults(geoResult, sooinResult) {
const resultBody = document.getElementById('geoResultBody');
const geoItems = geoResult.items || [];
const sooinItems = sooinResult.items || [];
// 재고 있는 것 먼저 정렬
items.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
geoItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
sooinItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
let html = `<table class="geo-table">
<thead>
<tr>
<th>제품명</th>
<th>규격</th>
<th>재고</th>
<th></th>
</tr>
</thead>
<tbody>`;
let html = '';
items.forEach((item, idx) => {
const hasStock = item.stock > 0;
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.product_name)}</span>
<span class="geo-code">${item.insurance_code}</span>
</div>
</td>
<td class="geo-spec">${item.specification}</td>
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
<td>
${hasStock ? `<button class="geo-add-btn" onclick="addGeoyoungToCart(${idx})">담기</button>` : ''}
</td>
</tr>`;
});
// ═══════ 지오영 섹션 ═══════
html += `<div class="ws-section">
<div class="ws-header geoyoung">
<span class="ws-icon">🏭</span>
<span class="ws-name">지오영</span>
<span class="ws-count">${geoItems.length}건</span>
</div>`;
html += '</tbody></table>';
if (geoItems.length > 0) {
html += `<table class="geo-table">
<thead><tr><th>제품명</th><th>규격</th><th>재고</th><th></th></tr></thead>
<tbody>`;
geoItems.forEach((item, idx) => {
const hasStock = item.stock > 0;
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.product_name)}</span>
<span class="geo-code">${item.insurance_code}</span>
</div>
</td>
<td class="geo-spec">${item.specification}</td>
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
<td>${hasStock ? `<button class="geo-add-btn" onclick="addToCartFromWholesale('geoyoung', ${idx})">담기</button>` : ''}</td>
</tr>`;
});
html += '</tbody></table>';
} else {
html += `<div class="ws-empty">📭 검색 결과 없음</div>`;
}
html += '</div>';
// 전역에 저장 (담기용)
window.geoyoungItems = items;
// ═══════ 수인약품 섹션 ═══════
html += `<div class="ws-section">
<div class="ws-header sooin">
<span class="ws-icon">💊</span>
<span class="ws-name">수인약품</span>
<span class="ws-count">${sooinItems.length}건</span>
</div>`;
if (sooinItems.length > 0) {
html += `<table class="geo-table">
<thead><tr><th>제품명</th><th>규격</th><th>단가</th><th>재고</th><th></th></tr></thead>
<tbody>`;
sooinItems.forEach((item, idx) => {
const hasStock = item.stock > 0;
html += `
<tr class="${hasStock ? '' : 'no-stock'}">
<td>
<div class="geo-product">
<span class="geo-name">${escapeHtml(item.name)}</span>
<span class="geo-code">${item.code} · ${item.manufacturer || ''}</span>
</div>
</td>
<td class="geo-spec">${item.spec || '-'}</td>
<td class="geo-price">${item.price ? item.price.toLocaleString() + '원' : '-'}</td>
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
<td>${hasStock ? `<button class="geo-add-btn sooin" onclick="addToCartFromWholesale('sooin', ${idx})">담기</button>` : ''}</td>
</tr>`;
});
html += '</tbody></table>';
} else {
html += `<div class="ws-empty">📭 검색 결과 없음</div>`;
}
html += '</div>';
resultBody.innerHTML = html;
}
function addGeoyoungToCart(idx) {
const item = window.geoyoungItems[idx];
if (!item || !currentGeoyoungItem) return;
function addToCartFromWholesale(wholesaler, idx) {
const items = window.wholesaleItems[wholesaler];
const item = items[idx];
if (!item || !currentWholesaleItem) return;
// 수량 계산 (규격에서 숫자 추출)
const specMatch = item.specification.match(/(\d+)/);
// 규격에서 수량 추출
const spec = wholesaler === 'geoyoung' ? item.specification : (item.spec || '');
const specMatch = spec.match(/(\d+)/);
const specQty = specMatch ? parseInt(specMatch[1]) : 1;
// 필요 수량 계산
const needed = currentGeoyoungItem.total_dose;
const needed = currentWholesaleItem.total_dose;
const suggestedQty = Math.ceil(needed / specQty);
const qty = prompt(`주문 수량 (${item.specification} 기준)\n\n필요량: ${needed}개\n규격: ${specQty}개/단위\n추천: ${suggestedQty}단위 (${suggestedQty * specQty}개)`, suggestedQty);
const supplierName = wholesaler === 'geoyoung' ? '지오영' : '수인약품';
const productName = wholesaler === 'geoyoung' ? item.product_name : item.name;
const qty = prompt(`[${supplierName}] 주문 수량 (${spec} 기준)\n\n필요량: ${needed}개\n규격: ${specQty}개/단위\n추천: ${suggestedQty}단위 (${suggestedQty * specQty}개)`, suggestedQty);
if (!qty || isNaN(qty)) return;
// 장바구니에 추가 (지오영 정보 포함)
// 장바구니에 추가
const cartItem = {
drug_code: currentGeoyoungItem.drug_code,
product_name: item.product_name,
supplier: '지오영',
drug_code: currentWholesaleItem.drug_code,
product_name: productName,
supplier: supplierName,
qty: parseInt(qty),
specification: item.specification,
geoyoung_code: item.insurance_code
specification: spec,
wholesaler: wholesaler,
internal_code: wholesaler === 'geoyoung' ? item.internal_code : item.internal_code,
geoyoung_code: wholesaler === 'geoyoung' ? item.insurance_code : null,
sooin_code: wholesaler === 'sooin' ? item.code : null
};
// 기존 항목 체크
const existing = cart.find(c => c.drug_code === currentGeoyoungItem.drug_code && c.specification === item.specification);
// 기존 항목 체크 (같은 도매상 + 같은 규격)
const existing = cart.find(c =>
c.drug_code === currentWholesaleItem.drug_code &&
c.specification === spec &&
c.wholesaler === wholesaler
);
if (existing) {
existing.qty = parseInt(qty);
} else {
@ -1444,24 +1496,28 @@
updateCartUI();
closeGeoyoungModal();
showToast(`✅ ${item.product_name} (${item.specification}) ${qty}개 추가`, 'success');
showToast(`✅ [${supplierName}] ${productName} (${spec}) ${qty}개 추가`, 'success');
}
// 테이블 행 더블클릭으로 지오영 모달 열기
// 하위 호환 (기존 함수명 유지)
function openGeoyoungModal(idx) { openWholesaleModal(idx); }
function addGeoyoungToCart(idx) { addToCartFromWholesale('geoyoung', idx); }
// 테이블 행 더블클릭으로 도매상 모달 열기
document.addEventListener('dblclick', function(e) {
const row = e.target.closest('tr[data-idx]');
if (row) {
const idx = parseInt(row.dataset.idx);
openGeoyoungModal(idx);
openWholesaleModal(idx);
}
});
</script>
<!-- 지오영 재고 조회 모달 -->
<!-- 도매상 재고 조회 모달 (지오영 + 수인) -->
<div class="geo-modal" id="geoyoungModal">
<div class="geo-modal-content">
<div class="geo-modal-header">
<h3>🏭 지오영 재고 조회</h3>
<h3>📦 도매상 재고 조회</h3>
<button class="geo-close" onclick="closeGeoyoungModal()"></button>
</div>
<div class="geo-modal-info">
@ -1649,6 +1705,62 @@
background: #0891b2;
transform: scale(1.05);
}
.geo-add-btn.sooin {
background: var(--accent-purple);
}
.geo-add-btn.sooin:hover {
background: #7c3aed;
}
.geo-price {
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
color: var(--text-secondary);
}
/* 도매상 섹션 구분 */
.ws-section {
margin-bottom: 20px;
}
.ws-section:last-child {
margin-bottom: 0;
}
.ws-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 8px;
margin-bottom: 8px;
font-weight: 600;
}
.ws-header.geoyoung {
background: linear-gradient(135deg, rgba(6, 182, 212, 0.2), rgba(8, 145, 178, 0.1));
border-left: 3px solid var(--accent-cyan);
}
.ws-header.sooin {
background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(124, 58, 237, 0.1));
border-left: 3px solid var(--accent-purple);
}
.ws-icon {
font-size: 18px;
}
.ws-name {
flex: 1;
font-size: 14px;
}
.ws-count {
font-size: 12px;
color: var(--text-muted);
background: rgba(255,255,255,0.1);
padding: 2px 8px;
border-radius: 10px;
}
.ws-empty {
text-align: center;
padding: 20px;
color: var(--text-muted);
font-size: 13px;
}
/* 주문 확인 모달 */
.order-modal {