feat: 동물약 안내서 기능 추가
- 동물약 뱃지 클릭 시 약품 정보 모달 표시 - APC 코드로 PostgreSQL 조회 (효능효과, 용법용량, 주의사항) - HTML 태그 파싱하여 텍스트 표시 - ESC/POS 인쇄 API 준비 (프린터 연결 시 활성화) - 미리보기 API: /api/animal-drug-info/preview - 인쇄 API: /api/animal-drug-info/print
This commit is contained in:
parent
77c667e1f6
commit
321fd0de1e
247
backend/app.py
247
backend/app.py
@ -6417,6 +6417,253 @@ def api_product_images_stats():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 동물약 정보 인쇄 API (ESC/POS 80mm)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.route('/api/animal-drug-info/print', methods=['POST'])
|
||||
def api_animal_drug_info_print():
|
||||
"""동물약 정보 인쇄 (APC로 PostgreSQL 조회 후 ESC/POS 출력)"""
|
||||
try:
|
||||
import re
|
||||
from html import unescape
|
||||
|
||||
data = request.get_json()
|
||||
apc = data.get('apc', '')
|
||||
product_name = data.get('product_name', '')
|
||||
|
||||
if not apc:
|
||||
return jsonify({'success': False, 'error': 'APC 코드가 필요합니다'}), 400
|
||||
|
||||
# PostgreSQL에서 약품 정보 조회
|
||||
try:
|
||||
from sqlalchemy import create_engine
|
||||
pg_engine = create_engine(
|
||||
'postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master',
|
||||
connect_args={'connect_timeout': 5}
|
||||
)
|
||||
|
||||
with pg_engine.connect() as conn:
|
||||
result = conn.execute(text("""
|
||||
SELECT
|
||||
product_name,
|
||||
company_name,
|
||||
main_ingredient,
|
||||
efficacy_effect,
|
||||
dosage_instructions,
|
||||
precautions,
|
||||
weight_min_kg,
|
||||
weight_max_kg,
|
||||
pet_size_label
|
||||
FROM apc
|
||||
WHERE apc = :apc
|
||||
LIMIT 1
|
||||
"""), {'apc': apc})
|
||||
row = result.fetchone()
|
||||
|
||||
if not row:
|
||||
return jsonify({'success': False, 'error': f'APC {apc} 정보를 찾을 수 없습니다'}), 404
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"PostgreSQL 조회 오류: {e}")
|
||||
return jsonify({'success': False, 'error': f'DB 조회 오류: {str(e)}'}), 500
|
||||
|
||||
# HTML 태그 제거 함수
|
||||
def strip_html(html_text):
|
||||
if not html_text:
|
||||
return ''
|
||||
# HTML 태그 제거
|
||||
text = re.sub(r'<[^>]+>', '', html_text)
|
||||
# HTML 엔티티 변환
|
||||
text = unescape(text)
|
||||
# 연속 공백/줄바꿈 정리
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
return text
|
||||
|
||||
# 텍스트를 줄 단위로 분리 (80mm ≈ 42자)
|
||||
def wrap_text(text, width=40):
|
||||
lines = []
|
||||
words = text.split()
|
||||
current_line = ""
|
||||
for word in words:
|
||||
if len(current_line) + len(word) + 1 <= width:
|
||||
current_line += (" " if current_line else "") + word
|
||||
else:
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
current_line = word
|
||||
if current_line:
|
||||
lines.append(current_line)
|
||||
return lines
|
||||
|
||||
# 데이터 파싱
|
||||
pg_product_name = row.product_name or product_name
|
||||
company = row.company_name or ''
|
||||
ingredient = row.main_ingredient or ''
|
||||
efficacy = strip_html(row.efficacy_effect)
|
||||
dosage = strip_html(row.dosage_instructions)
|
||||
precautions = strip_html(row.precautions)
|
||||
|
||||
# ESC/POS 인쇄 데이터 생성
|
||||
from escpos.printer import Usb
|
||||
|
||||
# 프린터 연결 (아침에 설정한 영수증 프린터)
|
||||
try:
|
||||
# POS 프린터 (VID/PID 확인 필요)
|
||||
printer = Usb(0x0483, 0x5743, profile="default")
|
||||
except Exception as e:
|
||||
logging.error(f"프린터 연결 실패: {e}")
|
||||
return jsonify({'success': False, 'error': f'프린터 연결 실패: {str(e)}'}), 500
|
||||
|
||||
try:
|
||||
# 헤더
|
||||
printer.set(align='center', bold=True, double_height=True)
|
||||
printer.text("🐾 동물약 안내서\n")
|
||||
printer.set(align='center', bold=False, double_height=False)
|
||||
printer.text("=" * 42 + "\n\n")
|
||||
|
||||
# 제품명
|
||||
printer.set(align='center', bold=True)
|
||||
printer.text(f"{pg_product_name}\n")
|
||||
printer.set(bold=False)
|
||||
if company:
|
||||
printer.text(f"제조: {company}\n")
|
||||
printer.text("\n")
|
||||
|
||||
# 주성분
|
||||
if ingredient and ingredient != 'NaN':
|
||||
printer.set(align='left')
|
||||
printer.text("-" * 42 + "\n")
|
||||
printer.set(bold=True)
|
||||
printer.text("▶ 주성분\n")
|
||||
printer.set(bold=False)
|
||||
for line in wrap_text(ingredient):
|
||||
printer.text(f" {line}\n")
|
||||
printer.text("\n")
|
||||
|
||||
# 효능효과
|
||||
if efficacy:
|
||||
printer.set(align='left')
|
||||
printer.text("-" * 42 + "\n")
|
||||
printer.set(bold=True)
|
||||
printer.text("▶ 효능효과\n")
|
||||
printer.set(bold=False)
|
||||
for line in wrap_text(efficacy[:300]): # 최대 300자
|
||||
printer.text(f" {line}\n")
|
||||
printer.text("\n")
|
||||
|
||||
# 용법용량
|
||||
if dosage:
|
||||
printer.text("-" * 42 + "\n")
|
||||
printer.set(bold=True)
|
||||
printer.text("▶ 용법용량\n")
|
||||
printer.set(bold=False)
|
||||
for line in wrap_text(dosage[:400]): # 최대 400자
|
||||
printer.text(f" {line}\n")
|
||||
printer.text("\n")
|
||||
|
||||
# 주의사항
|
||||
if precautions:
|
||||
printer.text("-" * 42 + "\n")
|
||||
printer.set(bold=True)
|
||||
printer.text("▶ 주의사항\n")
|
||||
printer.set(bold=False)
|
||||
for line in wrap_text(precautions[:300]): # 최대 300자
|
||||
printer.text(f" {line}\n")
|
||||
printer.text("\n")
|
||||
|
||||
# 푸터
|
||||
printer.set(align='center')
|
||||
printer.text("=" * 42 + "\n")
|
||||
printer.text("청 춘 약 국\n")
|
||||
printer.text("Tel: 033-481-5222\n\n")
|
||||
|
||||
# 커팅
|
||||
printer.cut()
|
||||
printer.close()
|
||||
|
||||
return jsonify({'success': True, 'message': '동물약 안내서 인쇄 완료'})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"인쇄 오류: {e}")
|
||||
try:
|
||||
printer.close()
|
||||
except:
|
||||
pass
|
||||
return jsonify({'success': False, 'error': f'인쇄 오류: {str(e)}'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"동물약 정보 인쇄 API 오류: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/animal-drug-info/preview', methods=['POST'])
|
||||
def api_animal_drug_info_preview():
|
||||
"""동물약 정보 미리보기 (텍스트 반환)"""
|
||||
try:
|
||||
import re
|
||||
from html import unescape
|
||||
|
||||
data = request.get_json()
|
||||
apc = data.get('apc', '')
|
||||
|
||||
if not apc:
|
||||
return jsonify({'success': False, 'error': 'APC 코드가 필요합니다'}), 400
|
||||
|
||||
# PostgreSQL에서 약품 정보 조회
|
||||
try:
|
||||
from sqlalchemy import create_engine
|
||||
pg_engine = create_engine(
|
||||
'postgresql://admin:trajet6640@192.168.0.87:5432/apdb_master',
|
||||
connect_args={'connect_timeout': 5}
|
||||
)
|
||||
|
||||
with pg_engine.connect() as conn:
|
||||
result = conn.execute(text("""
|
||||
SELECT
|
||||
product_name,
|
||||
company_name,
|
||||
main_ingredient,
|
||||
efficacy_effect,
|
||||
dosage_instructions,
|
||||
precautions
|
||||
FROM apc
|
||||
WHERE apc = :apc
|
||||
LIMIT 1
|
||||
"""), {'apc': apc})
|
||||
row = result.fetchone()
|
||||
|
||||
if not row:
|
||||
return jsonify({'success': False, 'error': f'APC {apc} 정보 없음'}), 404
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': f'DB 오류: {str(e)}'}), 500
|
||||
|
||||
# HTML 태그 제거
|
||||
def strip_html(html_text):
|
||||
if not html_text:
|
||||
return ''
|
||||
text = re.sub(r'<[^>]+>', '', html_text)
|
||||
text = unescape(text)
|
||||
text = re.sub(r'\s+', ' ', text).strip()
|
||||
return text
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'product_name': row.product_name,
|
||||
'company_name': row.company_name,
|
||||
'main_ingredient': row.main_ingredient if row.main_ingredient != 'NaN' else None,
|
||||
'efficacy_effect': strip_html(row.efficacy_effect),
|
||||
'dosage_instructions': strip_html(row.dosage_instructions),
|
||||
'precautions': strip_html(row.precautions)
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import os
|
||||
|
||||
|
||||
@ -417,6 +417,24 @@
|
||||
border-radius: 9px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.animal-badge {
|
||||
display: inline-block;
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
.animal-badge.clickable {
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.animal-badge.clickable:hover {
|
||||
background: #059669;
|
||||
box-shadow: 0 2px 8px rgba(16,185,129,0.4);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.code-na {
|
||||
background: #f1f5f9;
|
||||
color: #94a3b8;
|
||||
@ -1061,7 +1079,7 @@
|
||||
<td>
|
||||
<div class="product-name">
|
||||
${escapeHtml(item.product_name)}
|
||||
${item.is_animal_drug ? '<span style="display:inline-block;background:#10b981;color:#fff;font-size:11px;padding:2px 6px;border-radius:4px;margin-left:6px;">🐾 동물약</span>' : ''}
|
||||
${item.is_animal_drug ? `<span class="animal-badge ${item.apc ? 'clickable' : ''}" ${item.apc ? `onclick="printAnimalDrugInfo('${item.apc}', '${escapeHtml(item.product_name)}')" title="클릭하면 약품 안내서 인쇄"` : 'title="APC 없음"'}>🐾 동물약</span>` : ''}
|
||||
${categoryBadge}
|
||||
</div>
|
||||
<div class="product-supplier ${item.is_set ? 'set' : ''}">${escapeHtml(item.supplier) || ''}</div>
|
||||
@ -1085,6 +1103,101 @@
|
||||
`}).join('');
|
||||
}
|
||||
|
||||
// ── 동물약 안내서 ──
|
||||
async function printAnimalDrugInfo(apc, productName) {
|
||||
if (!apc) {
|
||||
alert('APC 코드가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 미리보기 API 호출
|
||||
try {
|
||||
const res = await fetch('/api/animal-drug-info/preview', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ apc })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
showAnimalDrugModal(data.data, apc);
|
||||
} else {
|
||||
alert(`정보 조회 실패: ${data.error}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`조회 오류: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function showAnimalDrugModal(info, apc) {
|
||||
// 기존 모달이 있으면 제거
|
||||
let modal = document.getElementById('animalDrugModal');
|
||||
if (modal) modal.remove();
|
||||
|
||||
// 모달 생성
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'animalDrugModal';
|
||||
modal.className = 'modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width:500px;max-height:80vh;overflow-y:auto;">
|
||||
<h3 style="margin:0 0 15px;color:#7c3aed;">🐾 동물약 안내서</h3>
|
||||
<div style="background:#f8fafc;padding:15px;border-radius:8px;margin-bottom:15px;">
|
||||
<h4 style="margin:0 0 8px;color:#1e293b;">${escapeHtml(info.product_name)}</h4>
|
||||
<p style="margin:0;color:#64748b;font-size:13px;">제조: ${escapeHtml(info.company_name || '-')}</p>
|
||||
${info.main_ingredient ? `<p style="margin:8px 0 0;color:#64748b;font-size:12px;">주성분: ${escapeHtml(info.main_ingredient)}</p>` : ''}
|
||||
</div>
|
||||
|
||||
${info.efficacy_effect ? `
|
||||
<div style="margin-bottom:12px;">
|
||||
<h5 style="margin:0 0 6px;color:#059669;">▶ 효능효과</h5>
|
||||
<p style="margin:0;font-size:13px;line-height:1.5;color:#334155;">${escapeHtml(info.efficacy_effect)}</p>
|
||||
</div>` : ''}
|
||||
|
||||
${info.dosage_instructions ? `
|
||||
<div style="margin-bottom:12px;">
|
||||
<h5 style="margin:0 0 6px;color:#0284c7;">▶ 용법용량</h5>
|
||||
<p style="margin:0;font-size:13px;line-height:1.5;color:#334155;">${escapeHtml(info.dosage_instructions)}</p>
|
||||
</div>` : ''}
|
||||
|
||||
${info.precautions ? `
|
||||
<div style="margin-bottom:12px;">
|
||||
<h5 style="margin:0 0 6px;color:#dc2626;">▶ 주의사항</h5>
|
||||
<p style="margin:0;font-size:13px;line-height:1.5;color:#334155;">${escapeHtml(info.precautions.substring(0, 500))}${info.precautions.length > 500 ? '...' : ''}</p>
|
||||
</div>` : ''}
|
||||
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button onclick="document.getElementById('animalDrugModal').remove()"
|
||||
style="flex:1;padding:10px;border:1px solid #e2e8f0;background:#fff;border-radius:6px;cursor:pointer;">닫기</button>
|
||||
<button onclick="printAnimalDrugSheet('${apc}')"
|
||||
style="flex:1;padding:10px;border:none;background:#7c3aed;color:#fff;border-radius:6px;cursor:pointer;">🖨️ 인쇄</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
modal.onclick = (e) => { if (e.target === modal) modal.remove(); };
|
||||
document.body.appendChild(modal);
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
async function printAnimalDrugSheet(apc) {
|
||||
try {
|
||||
const res = await fetch('/api/animal-drug-info/print', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({ apc })
|
||||
});
|
||||
const data = await res.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('인쇄 완료!');
|
||||
document.getElementById('animalDrugModal').remove();
|
||||
} else {
|
||||
alert(`인쇄 실패: ${data.error}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`인쇄 오류: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── QR 인쇄 관련 ──
|
||||
function adjustQty(delta) {
|
||||
printQty = Math.max(MIN_QTY, Math.min(MAX_QTY, printQty + delta));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user