feat: 실시간 POS 품목별 OTC 용법 라벨 인쇄 버튼 추가

POS 상세 패널:
- 품목 목록에 💊 인쇄 버튼 추가
- 프리셋 있으면 → 바로 인쇄
- 프리셋 없으면 → 새 창으로 등록 페이지 열기

API:
- /api/admin/pos-live/detail에 barcode 필드 추가

OTC 라벨 관리 페이지:
- URL 파라미터(barcode, name) 자동 처리
- POS에서 넘어올 때 자동으로 해당 약품 로드
This commit is contained in:
thug0bin 2026-03-02 17:14:45 +09:00
parent c154537c87
commit 887aba3a03
3 changed files with 103 additions and 3 deletions

View File

@ -5070,14 +5070,15 @@ def api_admin_pos_live_detail(order_no):
mssql_conn = mssql_engine.raw_connection()
cursor = mssql_conn.cursor()
# 품목 상세 조회
# 품목 상세 조회 (바코드 포함)
cursor.execute("""
SELECT
S.DrugCode AS drug_code,
ISNULL(G.GoodsName, '(약품명 없음)') AS product_name,
S.SL_NM_item AS quantity,
S.SL_NM_cost_a AS unit_price,
S.SL_TOTAL_PRICE AS total_price
S.SL_TOTAL_PRICE AS total_price,
G.Barcode AS barcode
FROM SALE_SUB S
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON S.DrugCode = G.DrugCode
WHERE S.SL_NO_order = ?
@ -5092,7 +5093,8 @@ def api_admin_pos_live_detail(order_no):
'product_name': row[1],
'quantity': int(row[2]) if row[2] else 0,
'unit_price': float(row[3]) if row[3] else 0,
'total_price': float(row[4]) if row[4] else 0
'total_price': float(row[4]) if row[4] else 0,
'barcode': row[5] or ''
})
return jsonify({

View File

@ -401,6 +401,30 @@
debounceTimer = setTimeout(previewLabel, 500);
});
});
// URL 파라미터로 바코드/이름 전달 시 자동 로드
const params = new URLSearchParams(window.location.search);
const urlBarcode = params.get('barcode');
const urlName = params.get('name');
if (urlBarcode) {
currentBarcode = urlBarcode;
currentDrugName = urlName || urlBarcode;
document.getElementById('barcode').value = urlBarcode;
document.getElementById('searchInput').value = urlName || urlBarcode;
// 기존 프리셋 확인
fetch(`/api/admin/otc-labels/${urlBarcode}`)
.then(res => res.json())
.then(data => {
if (data.exists) {
document.getElementById('displayName').value = data.label.display_name || '';
document.getElementById('effect').value = data.label.effect || '';
document.getElementById('dosageInstruction').value = data.label.dosage_instruction || '';
document.getElementById('usageTip').value = data.label.usage_tip || '';
}
previewLabel();
});
}
});
// 약품 검색 (MSSQL)

View File

@ -400,6 +400,24 @@
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
}
.item-label-btn {
padding: 4px 8px;
background: linear-gradient(135deg, #f59e0b, #d97706);
border: none;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
margin-left: 8px;
transition: all 0.2s;
}
.item-label-btn:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(245,158,11,0.4);
}
.item-label-btn:disabled {
opacity: 0.6;
cursor: wait;
}
/* ── 오버레이 ── */
.overlay {
@ -1139,6 +1157,7 @@
<span class="detail-item-name">${item.product_name}</span>
<span class="detail-item-qty">×${item.quantity}</span>
<span class="detail-item-price">₩${Math.floor(item.total_price).toLocaleString()}</span>
${item.barcode ? `<button class="item-label-btn" onclick="printOtcLabel('${item.barcode}', '${escapeHtml(item.product_name)}')">💊</button>` : ''}
</div>
`).join('');
document.getElementById('itemsList').innerHTML = itemsHtml;
@ -1149,6 +1168,61 @@
document.getElementById('itemsList').innerHTML = `<div class="empty-state">오류: ${err.message}</div>`;
}
}
// OTC 용법 라벨 인쇄
async function printOtcLabel(barcode, productName) {
const btn = event.target;
btn.disabled = true;
btn.textContent = '...';
try {
// 프리셋 확인
const checkRes = await fetch(`/api/admin/otc-labels/${barcode}`);
const checkData = await checkRes.json();
if (checkData.exists) {
// 프리셋 있음 → 바로 인쇄
const preset = checkData.label;
const printRes = await fetch('/api/admin/otc-labels/print', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
barcode: barcode,
drug_name: preset.display_name || productName,
effect: preset.effect || '',
dosage_instruction: preset.dosage_instruction || '',
usage_tip: preset.usage_tip || ''
})
});
const printData = await printRes.json();
if (printData.success) {
btn.textContent = '✓';
btn.style.background = '#10b981';
showToast(`용법 라벨 인쇄 완료!`, 'success');
} else {
btn.textContent = '💊';
btn.disabled = false;
showToast(printData.error || '인쇄 실패', 'error');
}
} else {
// 프리셋 없음 → 새 창으로 등록 페이지 열기
btn.textContent = '💊';
btn.disabled = false;
showToast('프리셋 미등록 → 등록 페이지로 이동', 'info');
window.open(`/admin/otc-labels?barcode=${barcode}&name=${encodeURIComponent(productName)}`, '_blank', 'width=1200,height=800');
}
} catch (err) {
btn.textContent = '💊';
btn.disabled = false;
showToast('오류: ' + err.message, 'error');
}
}
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'})[m]);
}
// ═══════════════════════════════════════════════════════════════
// 상세 패널에서 단일 건 처리