kdrug-inventory-system/static/medicine_master.js
시골약사 9dd1f41bbb feat: 의약품 마스터 입고 장바구니 UI 구현
검색 결과에서 제품을 장바구니에 담고, 종이 입고장 기준으로
수량/단가 입력 시 g당단가·금액을 자동 계산하는 프론트엔드 플로우.
DB 연동은 추후 구현 예정.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:11:56 +00:00

451 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 의약품 마스터 검색 + 입고 장바구니 모듈
*/
(function() {
'use strict';
// 장바구니 데이터 (세션 메모리)
let cart = [];
const ORIGIN_OPTIONS = ['한국','중국','베트남','인도','태국','페루','일본','기타'];
// ─── 검색 ───────────────────────────────────────────────
function searchMedicine() {
const query = $('#medSearchInput').val().trim();
const category = $('#medCategoryFilter').val();
const packageType = $('#medPackageFilter').val();
if (query.length < 2) {
alert('검색어는 2자 이상 입력하세요.');
return;
}
const params = new URLSearchParams({ q: query, limit: 100 });
if (category) params.append('category', category);
if (packageType) params.append('package_type', packageType);
$('#medSearchResults').html('<tr><td colspan="8" class="text-center py-4"><div class="spinner-border spinner-border-sm"></div> 검색중...</td></tr>');
$.get(`/api/medicine-master/search?${params}`, function(response) {
if (!response.success) {
$('#medSearchResults').html(`<tr><td colspan="8" class="text-center text-danger py-4">${response.error}</td></tr>`);
return;
}
const tbody = $('#medSearchResults');
tbody.empty();
$('#medResultCount').text(response.count);
if (response.data.length === 0) {
tbody.html('<tr><td colspan="8" class="text-center text-muted py-4">검색 결과가 없습니다.</td></tr>');
return;
}
response.data.forEach(item => {
let categoryBadge = '';
switch(item.category) {
case '일반의약품':
categoryBadge = '<span class="badge bg-success">일반</span>';
break;
case '전문의약품':
categoryBadge = '<span class="badge bg-warning text-dark">전문</span>';
break;
case '한약재':
categoryBadge = '<span class="badge bg-info">한약재</span>';
break;
case '원료의약품':
categoryBadge = '<span class="badge bg-secondary">원료</span>';
break;
default:
categoryBadge = `<span class="badge bg-light text-dark">${item.category || '-'}</span>`;
}
const cleanName = item.product_name.replace(/&nbsp;/g, ' ').trim();
const notes = item.notes ? item.notes.replace(/&nbsp;/g, ' ').trim() : '';
const inCart = cart.some(c => c.standard_code === item.standard_code);
const itemJson = JSON.stringify(item).replace(/'/g, "&#39;");
tbody.append(`
<tr>
<td>
<strong>${cleanName}</strong>
${item.form_type ? `<br><small class="text-muted">${item.form_type}</small>` : ''}
</td>
<td><small>${item.company_name || '-'}</small></td>
<td><small>${item.spec || '-'}</small></td>
<td><small>${item.package_type || '-'}</small></td>
<td>${categoryBadge}</td>
<td><small class="text-monospace">${item.standard_code || '-'}</small></td>
<td><small class="text-muted">${notes.length > 30 ? notes.substring(0, 30) + '...' : notes}</small></td>
<td class="text-nowrap">
<button class="btn btn-sm btn-outline-info med-detail-btn"
data-item='${itemJson}' title="상세보기">
<i class="bi bi-eye"></i>
</button>
<button class="btn btn-sm btn-outline-success med-add-cart-btn"
data-item='${itemJson}'
data-code="${item.standard_code}"
title="장바구니 담기"
${inCart ? 'disabled' : ''}>
<i class="bi bi-plus-lg"></i>
</button>
</td>
</tr>
`);
});
// 상세보기 이벤트
$('.med-detail-btn').off('click').on('click', function() {
const item = JSON.parse($(this).attr('data-item'));
showMedicineDetail(item);
});
// 장바구니 담기 이벤트
$('.med-add-cart-btn').off('click').on('click', function() {
const item = JSON.parse($(this).attr('data-item'));
addToCart(item);
$(this).prop('disabled', true);
});
}).fail(function(xhr) {
$('#medSearchResults').html(`<tr><td colspan="8" class="text-center text-danger py-4">검색 실패: ${xhr.responseJSON?.error || '서버 오류'}</td></tr>`);
});
}
// ─── 상세보기 모달 ─────────────────────────────────────
function showMedicineDetail(item) {
const cleanName = item.product_name.replace(/&nbsp;/g, ' ').trim();
$('#medDetailTitle').text(cleanName);
const fields = [
{ label: '상품명', value: cleanName },
{ label: '업체명', value: item.company_name },
{ label: '규격', value: item.spec },
{ label: '제형구분', value: item.form_type },
{ label: '포장형태', value: item.package_type },
{ label: '전문일반구분', value: item.category },
{ label: '품목기준코드', value: item.item_std_code },
{ label: '대표코드', value: item.representative_code },
{ label: '표준코드', value: item.standard_code },
{ label: '일반명코드', value: item.ingredient_name_code },
{ label: 'ATC코드', value: item.atc_code },
{ label: '비고', value: item.notes?.replace(/&nbsp;/g, ' ') },
];
let html = '<table class="table table-sm">';
fields.forEach(f => {
if (f.value) {
html += `<tr><th width="140" class="text-muted">${f.label}</th><td>${f.value}</td></tr>`;
}
});
html += '</table>';
$('#medDetailBody').html(html);
$('#medDetailModal').modal('show');
}
// ─── 규격 파싱 ───────────────────────────────────────────
// 규격 문자열에서 그램 수 추출 (예: "500그램"→500, "1000그램"→1000)
function parseSpecToGrams(spec) {
if (!spec) return 0;
const s = spec.trim();
// "500그램", "1000그램", "1252.5그램"
let m = s.match(/^([\d.]+)\s*그램$/);
if (m) return parseFloat(m[1]);
// "500g", "1000G"
m = s.match(/^([\d.]+)\s*[gG]$/);
if (m) return parseFloat(m[1]);
// "1kg", "1.5Kg", "1킬로그램"
m = s.match(/^([\d.]+)\s*(kg|킬로그램)$/i);
if (m) return parseFloat(m[1]) * 1000;
// "500밀리그램" → 0.5g
m = s.match(/^([\d.]+)\s*밀리그램$/);
if (m) return parseFloat(m[1]) / 1000;
// "500mg"
m = s.match(/^([\d.]+)\s*mg$/i);
if (m) return parseFloat(m[1]) / 1000;
return 0; // 파싱 불가 ("없음" 등)
}
// ─── 장바구니 기능 ─────────────────────────────────────
function addToCart(item) {
// 중복 체크
if (cart.some(c => c.standard_code === item.standard_code)) {
alert('이미 장바구니에 있는 항목입니다.');
return;
}
const cleanName = item.product_name.replace(/&nbsp;/g, ' ').trim();
const specGrams = parseSpecToGrams(item.spec);
cart.push({
standard_code: item.standard_code,
product_name: cleanName,
company_name: item.company_name || '',
spec: item.spec || '',
spec_grams: specGrams, // 규격에서 파싱한 g 수
qty: 1, // 수량 (포장 단위)
unit_price: 0, // 단가 (종이 입고장 가격)
origin_country: '한국',
_raw: item
});
renderCart();
}
function removeFromCart(index) {
const removed = cart.splice(index, 1)[0];
if (removed) {
$(`.med-add-cart-btn[data-code="${removed.standard_code}"]`).prop('disabled', false);
}
renderCart();
}
function clearCart() {
if (cart.length === 0) return;
if (!confirm('장바구니를 비우시겠습니까?')) return;
const codes = cart.map(c => c.standard_code);
cart = [];
codes.forEach(code => {
$(`.med-add-cart-btn[data-code="${code}"]`).prop('disabled', false);
});
renderCart();
}
function renderCart() {
const panel = $('#cartPanel');
const tbody = $('#cartBody');
if (cart.length === 0) {
panel.hide();
return;
}
panel.show();
$('#cartCount').text(cart.length);
tbody.empty();
let totalQty = 0;
let totalAmt = 0;
cart.forEach((item, idx) => {
const amt = (item.qty || 0) * (item.unit_price || 0);
const pricePerG = (item.spec_grams && item.unit_price)
? (item.unit_price / item.spec_grams) : 0;
totalQty += (item.qty || 0);
totalAmt += amt;
const originOptions = ORIGIN_OPTIONS.map(o =>
`<option value="${o}" ${item.origin_country === o ? 'selected' : ''}>${o}</option>`
).join('');
const specDisplay = item.spec_grams
? `${item.spec} <br><small class="text-success">${item.spec_grams.toLocaleString()}g</small>`
: `${item.spec || '-'} <br><small class="text-danger">수동입력</small>`;
tbody.append(`
<tr data-cart-idx="${idx}">
<td><small><strong>${item.product_name}</strong></small></td>
<td><small>${item.company_name || '-'}</small></td>
<td>${specDisplay}</td>
<td>
<input type="number" class="form-control form-control-sm text-end cart-qty"
data-idx="${idx}" value="${item.qty || 1}"
min="1" step="1" placeholder="1">
</td>
<td>
<input type="number" class="form-control form-control-sm text-end cart-unit-price"
data-idx="${idx}" value="${item.unit_price || ''}"
min="0" step="100" placeholder="35000">
</td>
<td class="text-end cart-amt" data-idx="${idx}">
${amt ? amt.toLocaleString() : '-'}
</td>
<td class="text-end cart-ppg text-muted" data-idx="${idx}">
<small>${pricePerG ? pricePerG.toFixed(1) : '-'}</small>
</td>
<td>
<select class="form-select form-select-sm cart-origin" data-idx="${idx}">
${originOptions}
</select>
</td>
<td>
<button class="btn btn-sm btn-outline-danger cart-remove-btn" data-idx="${idx}" title="삭제">
<i class="bi bi-x"></i>
</button>
</td>
</tr>
`);
});
$('#cartTotalQty').text(totalQty ? totalQty.toLocaleString() : '0');
$('#cartTotalAmt').text(totalAmt ? totalAmt.toLocaleString() + '원' : '0');
// 이벤트 바인딩
tbody.find('.cart-qty').off('input').on('input', function() {
const idx = $(this).data('idx');
cart[idx].qty = parseInt($(this).val()) || 0;
updateCartRow(idx);
});
tbody.find('.cart-unit-price').off('input').on('input', function() {
const idx = $(this).data('idx');
cart[idx].unit_price = parseFloat($(this).val()) || 0;
updateCartRow(idx);
});
tbody.find('.cart-origin').off('change').on('change', function() {
const idx = $(this).data('idx');
cart[idx].origin_country = $(this).val();
});
tbody.find('.cart-remove-btn').off('click').on('click', function() {
removeFromCart($(this).data('idx'));
});
}
function updateCartRow(idx) {
const item = cart[idx];
const amt = (item.qty || 0) * (item.unit_price || 0);
const pricePerG = (item.spec_grams && item.unit_price)
? (item.unit_price / item.spec_grams) : 0;
$(`.cart-amt[data-idx="${idx}"]`).text(amt ? amt.toLocaleString() : '-');
$(`.cart-ppg[data-idx="${idx}"]`).html(`<small>${pricePerG ? pricePerG.toFixed(1) : '-'}</small>`);
updateCartTotals();
}
function updateCartTotals() {
let totalQty = 0;
let totalAmt = 0;
cart.forEach(item => {
totalQty += (item.qty || 0);
totalAmt += (item.qty || 0) * (item.unit_price || 0);
});
$('#cartTotalQty').text(totalQty ? totalQty.toLocaleString() : '0');
$('#cartTotalAmt').text(totalAmt ? totalAmt.toLocaleString() + '원' : '0');
}
// ─── 입고장 생성 ───────────────────────────────────────
function createReceipt() {
// 검증
const supplierId = $('#cartSupplier').val();
const receiptDate = $('#cartReceiptDate').val();
const notes = $('#cartNotes').val().trim();
if (!supplierId) {
alert('도매상을 선택해주세요.');
$('#cartSupplier').focus();
return;
}
if (!receiptDate) {
alert('입고일을 입력해주세요.');
$('#cartReceiptDate').focus();
return;
}
if (cart.length === 0) {
alert('장바구니가 비어있습니다.');
return;
}
// 단가 미입력 체크
const incomplete = cart.filter(c => !c.unit_price);
if (incomplete.length > 0) {
alert(`단가가 입력되지 않은 항목이 ${incomplete.length}건 있습니다.\n모든 항목의 단가를 입력해주세요.`);
return;
}
// 규격 미파싱 경고
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 supplierName = $('#cartSupplier option:selected').text();
let totalAmt = 0;
cart.forEach(item => {
totalAmt += (item.qty || 0) * (item.unit_price || 0);
});
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');
alert(summary);
}
// ─── 도매상 로드 ───────────────────────────────────────
function loadSuppliers() {
$.get('/api/suppliers', function(response) {
if (!response.success) return;
const sel = $('#cartSupplier');
sel.find('option:not(:first)').remove();
response.data.forEach(s => {
sel.append(`<option value="${s.supplier_id}">${s.name}</option>`);
});
});
}
// ─── 초기화 ────────────────────────────────────────────
function init() {
// 검색
$('#medSearchBtn').off('click').on('click', searchMedicine);
$('#medSearchInput').off('keypress').on('keypress', function(e) {
if (e.which === 13) searchMedicine();
});
$('#medCategoryFilter, #medPackageFilter').off('change').on('change', function() {
if ($('#medSearchInput').val().trim().length >= 2) {
searchMedicine();
}
});
// 장바구니
$('#cartClearBtn').off('click').on('click', clearCart);
$('#createReceiptBtn').off('click').on('click', createReceipt);
// 입고일 기본값: 오늘
$('#cartReceiptDate').val(new Date().toISOString().split('T')[0]);
// 도매상 목록 로드
loadSuppliers();
// 이전 장바구니 상태 복원 (없으면 숨김)
renderCart();
}
// 글로벌 로드 함수 등록
window.loadMedicineMaster = function() {
init();
};
})();