From 321fd0de1e2118d83820ab540c4be596dd9a15c8 Mon Sep 17 00:00:00 2001 From: thug0bin Date: Wed, 4 Mar 2026 19:18:10 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8F=99=EB=AC=BC=EC=95=BD=20=EC=95=88?= =?UTF-8?q?=EB=82=B4=EC=84=9C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 동물약 뱃지 클릭 시 약품 정보 모달 표시 - APC 코드로 PostgreSQL 조회 (효능효과, 용법용량, 주의사항) - HTML 태그 파싱하여 텍스트 표시 - ESC/POS 인쇄 API 준비 (프린터 연결 시 활성화) - 미리보기 API: /api/animal-drug-info/preview - 인쇄 API: /api/animal-drug-info/print --- backend/app.py | 247 ++++++++++++++++++++++++++ backend/templates/admin_products.html | 115 +++++++++++- 2 files changed, 361 insertions(+), 1 deletion(-) diff --git a/backend/app.py b/backend/app.py index 586ff70..a097cec 100644 --- a/backend/app.py +++ b/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 diff --git a/backend/templates/admin_products.html b/backend/templates/admin_products.html index 8216d36..02b4887 100644 --- a/backend/templates/admin_products.html +++ b/backend/templates/admin_products.html @@ -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 @@
${escapeHtml(item.product_name)} - ${item.is_animal_drug ? '🐾 동물약' : ''} + ${item.is_animal_drug ? `🐾 동물약` : ''} ${categoryBadge}
${escapeHtml(item.supplier) || ''}
@@ -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 = ` + + `; + 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));