feat: 의약품 마스터 DB 연동 및 한약재/OTC 구분 체계 구축

- herb_items 테이블에 product_type, standard_code 컬럼 추가
- POST /api/purchase-receipts/from-cart API 구현 (표준코드 기반 입고)
- 5개 API에 product_type/standard_code 필드 추가
- 프론트엔드 전역 구분 표시: 한약재/OTC 배지, 보험코드/표준코드 구분
- 경방신약 주문 매핑 문서 작성 (38건, 총액 1,561,800원)
- DB 스키마 백업 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-02-23 13:39:59 +00:00
parent 9dd1f41bbb
commit 2ca5622bbd
8 changed files with 771 additions and 31 deletions

View File

@@ -116,6 +116,9 @@ $(document).ready(function() {
case 'herb-info':
loadHerbInfo();
break;
case 'medicine-master':
if (typeof loadMedicineMaster === 'function') loadMedicineMaster();
break;
}
}
@@ -2132,9 +2135,18 @@ $(document).ready(function() {
totalValue += item.total_value || 0;
if (item.total_quantity > 0) herbsInStock++;
const isOTC = item.product_type === 'OTC';
const typeBadge = isOTC
? '<span class="badge bg-warning text-dark">OTC</span>'
: '<span class="badge bg-info">한약재</span>';
const codeDisplay = isOTC
? (item.standard_code || '-')
: (item.insurance_code || '-');
tbody.append(`
<tr class="inventory-row" data-herb-id="${item.herb_item_id}">
<td>${item.insurance_code || '-'}</td>
<td>${typeBadge}</td>
<td><small class="text-monospace">${codeDisplay}</small></td>
<td>${item.herb_name}${originBadge}${efficacyTags}</td>
<td>${item.total_quantity.toFixed(1)}</td>
<td>${item.lot_count}</td>
@@ -2253,7 +2265,8 @@ $(document).ready(function() {
<div class="modal-header">
<h5 class="modal-title">
${data.herb_name} 재고 상세
<small class="text-muted">(${data.insurance_code})</small>
<small class="text-muted">(${data.product_type === 'OTC' ? data.standard_code : data.insurance_code})</small>
${data.product_type === 'OTC' ? '<span class="badge bg-warning text-dark ms-2">OTC</span>' : ''}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
@@ -2293,9 +2306,14 @@ $(document).ready(function() {
tbody.empty();
response.data.forEach(herb => {
const hIsOTC = herb.product_type === 'OTC';
const hCode = hIsOTC ? (herb.standard_code || '-') : (herb.insurance_code || '-');
const hTypeBadge = hIsOTC
? '<span class="badge bg-warning text-dark">OTC</span>'
: '<span class="badge bg-info">한약재</span>';
tbody.append(`
<tr>
<td>${herb.insurance_code || '-'}</td>
<td>${hTypeBadge} <small class="text-monospace">${hCode}</small></td>
<td>${herb.herb_name}</td>
<td>${herb.specification || '-'}</td>
<td>${herb.current_stock ? herb.current_stock.toFixed(1) + 'g' : '0g'}</td>
@@ -2394,14 +2412,24 @@ $(document).ready(function() {
if (line.processing) variantBadges += `<span class="badge bg-warning ms-1">${line.processing}</span>`;
if (line.grade) variantBadges += `<span class="badge bg-success ms-1">${line.grade}</span>`;
const lineIsOTC = line.product_type === 'OTC';
const lineTypeBadge = lineIsOTC
? '<span class="badge bg-warning text-dark me-1">OTC</span>'
: '<span class="badge bg-info me-1">한약재</span>';
const lineCode = lineIsOTC
? (line.standard_code || '-')
: (line.insurance_code || '-');
const lineCodeLabel = lineIsOTC ? '표준' : '보험';
linesHtml += `
<tr>
<td>
${lineTypeBadge}
<div>${line.herb_name}</div>
${line.display_name ? `<small class="text-primary">${line.display_name}</small>` : ''}
${variantBadges}
</td>
<td>${line.insurance_code || '-'}</td>
<td><small class="text-muted">${lineCodeLabel}</small><br><small class="text-monospace">${lineCode}</small></td>
<td>${line.origin_country || '-'}</td>
<td>${line.quantity_g}g</td>
<td>${formatCurrency(line.unit_price_per_g)}</td>
@@ -2428,8 +2456,8 @@ $(document).ready(function() {
<table class="table table-sm">
<thead>
<tr>
<th>약재명</th>
<th>보험코드</th>
<th>품목명</th>
<th>코드</th>
<th>원산지</th>
<th>수량</th>
<th>단가</th>
@@ -3468,7 +3496,7 @@ $(document).ready(function() {
itemsBody.append(`
<tr>
<td>${item.herb_name}</td>
<td>${item.insurance_code || '-'}</td>
<td>${item.product_type === 'OTC' ? (item.standard_code || '-') : (item.insurance_code || '-')}</td>
<td>${item.origin_country || '-'}</td>
<td>#${item.lot_id}</td>
<td>${item.quantity_before.toFixed(1)}g</td>

View File

@@ -367,13 +367,19 @@
return;
}
// 규격 미파싱 경고
// 규격 미파싱 경고 (spec_grams=0이면 g 환산 불가)
const noSpec = cart.filter(c => !c.spec_grams);
if (noSpec.length > 0) {
alert(`규격(g)을 파싱할 수 없는 항목이 ${noSpec.length}건 있습니다.\n해당 항목은 g당단가가 계산되지 않습니다.\n\n${noSpec.map(c => '- ' + c.product_name).join('\n')}`);
const ok = confirm(
`규격(g)을 파싱할 수 없는 항목이 ${noSpec.length}건 있습니다.\n` +
`해당 항목은 g당단가가 계산되지 않습니다.\n\n` +
noSpec.map(c => '- ' + c.product_name + ' (' + c.spec + ')').join('\n') +
'\n\n그래도 진행하시겠습니까?'
);
if (!ok) return;
}
// 요약 표시
// 확인 요약
const supplierName = $('#cartSupplier option:selected').text();
let totalAmt = 0;
cart.forEach(item => {
@@ -381,25 +387,65 @@
});
const summary = [
`[입고장 요약]`,
`도매상: ${supplierName}`,
`입고일: ${receiptDate}`,
`품목 수: ${cart.length}`,
`총 금액: ${totalAmt.toLocaleString()}`,
notes ? `비고: ${notes}` : '',
'',
'--- 품목 ---',
...cart.map((c, i) => {
const amt = c.qty * c.unit_price;
const ppg = c.spec_grams ? (c.unit_price / c.spec_grams).toFixed(1) : '?';
const totalG = c.spec_grams ? (c.spec_grams * c.qty).toLocaleString() + 'g' : '?';
return `${i+1}. ${c.product_name} (${c.spec}) ×${c.qty} @${c.unit_price.toLocaleString()}원 = ${amt.toLocaleString()}원 [${totalG}, ${ppg}원/g]`;
}),
'',
'(DB 연동은 추후 구현 예정입니다)'
].filter(Boolean).join('\n');
].join('\n');
alert(summary);
if (!confirm(`입고장을 생성하시겠습니까?\n\n${summary}`)) return;
// API 요청 데이터 구성
const payload = {
supplier_id: parseInt(supplierId),
receipt_date: receiptDate,
notes: notes,
lines: cart.map(c => ({
standard_code: c.standard_code,
product_name: c.product_name,
company_name: c.company_name,
spec: c.spec,
spec_grams: c.spec_grams,
qty: c.qty,
unit_price: c.unit_price,
origin_country: c.origin_country
}))
};
// 버튼 비활성화
const btn = $('#createReceiptBtn');
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> 처리중...');
$.ajax({
url: '/api/purchase-receipts/from-cart',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify(payload),
success: function(response) {
if (response.success) {
alert(
`입고 완료!\n\n` +
`입고장 번호: ${response.receipt_no}\n` +
`품목: ${response.summary.item_count}\n` +
`총 금액: ${response.summary.total_amount}`
);
// 장바구니 초기화
cart = [];
renderCart();
// 검색결과 버튼 전부 재활성화
$('.med-add-cart-btn').prop('disabled', false);
} else {
alert('입고 실패: ' + response.error);
}
},
error: function(xhr) {
const msg = xhr.responseJSON?.error || '서버 오류';
alert('입고 실패: ' + msg);
},
complete: function() {
btn.prop('disabled', false).html('<i class="bi bi-clipboard-check"></i> 입고장 생성');
}
});
}
// ─── 도매상 로드 ───────────────────────────────────────