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 @@