feat: 더블클릭 시 지오영+수인 동시 재고 조회
- openWholesaleModal: 두 도매상 동시 API 호출 - Promise.all로 병렬 조회 (빠른 응답) - 도매상별 섹션 구분 UI (지오영: 청록, 수인: 보라) - 각 도매상별 담기 버튼 - 수인은 단가 정보도 표시
This commit is contained in:
parent
90d993156e
commit
19c70e42fb
@ -1279,14 +1279,15 @@
|
|||||||
if (e.key === 'Enter') loadUsageData();
|
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];
|
const item = usageData[idx];
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
|
|
||||||
currentGeoyoungItem = item;
|
currentWholesaleItem = item;
|
||||||
|
|
||||||
// 모달 열기
|
// 모달 열기
|
||||||
document.getElementById('geoModalProductName').textContent = item.product_name;
|
document.getElementById('geoModalProductName').textContent = item.product_name;
|
||||||
@ -1300,89 +1301,92 @@
|
|||||||
document.getElementById('geoResultBody').innerHTML = `
|
document.getElementById('geoResultBody').innerHTML = `
|
||||||
<div class="geo-loading">
|
<div class="geo-loading">
|
||||||
<div class="loading-spinner"></div>
|
<div class="loading-spinner"></div>
|
||||||
<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() {
|
function closeGeoyoungModal() {
|
||||||
document.getElementById('geoyoungModal').classList.remove('show');
|
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 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 {
|
try {
|
||||||
// 1차: 보험코드(KD코드)로 검색
|
// 1차: 보험코드로 검색
|
||||||
let response = await fetch(`/api/geoyoung/stock?kd_code=${encodeURIComponent(kdCode)}`);
|
let response = await fetch(`/api/geoyoung/stock?kd_code=${encodeURIComponent(kdCode)}`);
|
||||||
let data = await response.json();
|
let data = await response.json();
|
||||||
|
|
||||||
// 결과 없으면 성분명으로 재검색
|
// 결과 없으면 성분명으로 재검색
|
||||||
if (data.success && data.count === 0) {
|
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)}`);
|
response = await fetch(`/api/geoyoung/stock-by-name?product_name=${encodeURIComponent(productName)}`);
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.success) {
|
return data;
|
||||||
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);
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
resultBody.innerHTML = `
|
return { success: false, error: err.message, items: [] };
|
||||||
<div class="geo-error">
|
|
||||||
<div>❌ 네트워크 오류: ${err.message}</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderGeoyoungResults(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 renderWholesaleResults(geoResult, sooinResult) {
|
||||||
const resultBody = document.getElementById('geoResultBody');
|
const resultBody = document.getElementById('geoResultBody');
|
||||||
|
|
||||||
// 재고 있는 것 먼저 정렬
|
const geoItems = geoResult.items || [];
|
||||||
items.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
|
const sooinItems = sooinResult.items || [];
|
||||||
|
|
||||||
let html = `<table class="geo-table">
|
// 재고 있는 것 먼저 정렬
|
||||||
<thead>
|
geoItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
|
||||||
<tr>
|
sooinItems.sort((a, b) => (b.stock > 0 ? 1 : 0) - (a.stock > 0 ? 1 : 0) || b.stock - a.stock);
|
||||||
<th>제품명</th>
|
|
||||||
<th>규격</th>
|
let html = '';
|
||||||
<th>재고</th>
|
|
||||||
<th></th>
|
// ═══════ 지오영 섹션 ═══════
|
||||||
</tr>
|
html += `<div class="ws-section">
|
||||||
</thead>
|
<div class="ws-header geoyoung">
|
||||||
|
<span class="ws-icon">🏭</span>
|
||||||
|
<span class="ws-name">지오영</span>
|
||||||
|
<span class="ws-count">${geoItems.length}건</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (geoItems.length > 0) {
|
||||||
|
html += `<table class="geo-table">
|
||||||
|
<thead><tr><th>제품명</th><th>규격</th><th>재고</th><th></th></tr></thead>
|
||||||
<tbody>`;
|
<tbody>`;
|
||||||
|
|
||||||
items.forEach((item, idx) => {
|
geoItems.forEach((item, idx) => {
|
||||||
const hasStock = item.stock > 0;
|
const hasStock = item.stock > 0;
|
||||||
html += `
|
html += `
|
||||||
<tr class="${hasStock ? '' : 'no-stock'}">
|
<tr class="${hasStock ? '' : 'no-stock'}">
|
||||||
@ -1394,48 +1398,96 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="geo-spec">${item.specification}</td>
|
<td class="geo-spec">${item.specification}</td>
|
||||||
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
|
<td class="geo-stock ${hasStock ? 'in-stock' : 'out-stock'}">${item.stock}</td>
|
||||||
<td>
|
<td>${hasStock ? `<button class="geo-add-btn" onclick="addToCartFromWholesale('geoyoung', ${idx})">담기</button>` : ''}</td>
|
||||||
${hasStock ? `<button class="geo-add-btn" onclick="addGeoyoungToCart(${idx})">담기</button>` : ''}
|
|
||||||
</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|
||||||
html += '</tbody></table>';
|
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;
|
resultBody.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addGeoyoungToCart(idx) {
|
function addToCartFromWholesale(wholesaler, idx) {
|
||||||
const item = window.geoyoungItems[idx];
|
const items = window.wholesaleItems[wholesaler];
|
||||||
if (!item || !currentGeoyoungItem) return;
|
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 specQty = specMatch ? parseInt(specMatch[1]) : 1;
|
||||||
|
|
||||||
// 필요 수량 계산
|
// 필요 수량 계산
|
||||||
const needed = currentGeoyoungItem.total_dose;
|
const needed = currentWholesaleItem.total_dose;
|
||||||
const suggestedQty = Math.ceil(needed / specQty);
|
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;
|
if (!qty || isNaN(qty)) return;
|
||||||
|
|
||||||
// 장바구니에 추가 (지오영 정보 포함)
|
// 장바구니에 추가
|
||||||
const cartItem = {
|
const cartItem = {
|
||||||
drug_code: currentGeoyoungItem.drug_code,
|
drug_code: currentWholesaleItem.drug_code,
|
||||||
product_name: item.product_name,
|
product_name: productName,
|
||||||
supplier: '지오영',
|
supplier: supplierName,
|
||||||
qty: parseInt(qty),
|
qty: parseInt(qty),
|
||||||
specification: item.specification,
|
specification: spec,
|
||||||
geoyoung_code: item.insurance_code
|
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) {
|
if (existing) {
|
||||||
existing.qty = parseInt(qty);
|
existing.qty = parseInt(qty);
|
||||||
} else {
|
} else {
|
||||||
@ -1444,24 +1496,28 @@
|
|||||||
|
|
||||||
updateCartUI();
|
updateCartUI();
|
||||||
closeGeoyoungModal();
|
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) {
|
document.addEventListener('dblclick', function(e) {
|
||||||
const row = e.target.closest('tr[data-idx]');
|
const row = e.target.closest('tr[data-idx]');
|
||||||
if (row) {
|
if (row) {
|
||||||
const idx = parseInt(row.dataset.idx);
|
const idx = parseInt(row.dataset.idx);
|
||||||
openGeoyoungModal(idx);
|
openWholesaleModal(idx);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- 지오영 재고 조회 모달 -->
|
<!-- 도매상 재고 조회 모달 (지오영 + 수인) -->
|
||||||
<div class="geo-modal" id="geoyoungModal">
|
<div class="geo-modal" id="geoyoungModal">
|
||||||
<div class="geo-modal-content">
|
<div class="geo-modal-content">
|
||||||
<div class="geo-modal-header">
|
<div class="geo-modal-header">
|
||||||
<h3>🏭 지오영 재고 조회</h3>
|
<h3>📦 도매상 재고 조회</h3>
|
||||||
<button class="geo-close" onclick="closeGeoyoungModal()">✕</button>
|
<button class="geo-close" onclick="closeGeoyoungModal()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="geo-modal-info">
|
<div class="geo-modal-info">
|
||||||
@ -1649,6 +1705,62 @@
|
|||||||
background: #0891b2;
|
background: #0891b2;
|
||||||
transform: scale(1.05);
|
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 {
|
.order-modal {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user