diff --git a/backend/app.py b/backend/app.py index ad19d4d..401bc44 100644 --- a/backend/app.py +++ b/backend/app.py @@ -16,7 +16,7 @@ if sys.platform == 'win32': sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8') os.environ.setdefault('PYTHONIOENCODING', 'utf-8') -from flask import Flask, request, render_template, render_template_string, jsonify, redirect, url_for, session +from flask import Flask, request, render_template, render_template_string, jsonify, redirect, url_for, session, make_response import hashlib import base64 import secrets @@ -8535,6 +8535,323 @@ def api_animal_drug_info_print(): return jsonify({'success': False, 'error': str(e)}), 500 +# ═══════════════════════════════════════════════════════════════════════════════ +# 동물약 정보 인쇄 API (GET - URL로 바로 인쇄) - 외부 CORS 지원 +# ═══════════════════════════════════════════════════════════════════════════════ + +@app.route('/api/animal-drug-print/') +def api_animal_drug_print_by_code(code): + """ + 동물약 안내서 바로 인쇄 (GET 방식) + GET /api/animal-drug-print/0231093520106 (APC 코드) + GET /api/animal-drug-print/8809720800035 (바코드) + + - CORS 지원 (외부에서 호출 가능) + - APC 또는 바코드로 조회 후 ESC/POS 프린터로 인쇄 + """ + try: + import re + from html import unescape + + apc = None + product_name = None + + # 코드 타입 판별 (APC vs 바코드) + if code.startswith('023'): + # APC 코드 + apc = code + else: + # 바코드로 가정 → APC 조회 + try: + conn_str = ( + 'DRIVER={ODBC Driver 17 for SQL Server};' + 'SERVER=192.168.0.4\\PM2014;' + 'DATABASE=PM_DRUG;' + 'UID=sa;' + 'PWD=tmddls214!%(;' + 'TrustServerCertificate=yes' + ) + import pyodbc + conn = pyodbc.connect(conn_str, timeout=10) + cursor = conn.cursor() + + # 바코드 → APC 조회 + cursor.execute(""" + SELECT TOP 1 + U.CD_CD_BARCODE as apc, + G.GoodsName as product_name + FROM CD_ITEM_UNIT_MEMBER U + JOIN CD_GOODS G ON U.DRUGCODE = G.DrugCode + WHERE U.CD_CD_BARCODE = ? OR G.BARCODE = ? + """, (code, code)) + row = cursor.fetchone() + conn.close() + + if row and row.apc: + apc = row.apc + product_name = row.product_name + else: + response = jsonify({'success': False, 'error': f'바코드 {code}에 해당하는 APC를 찾을 수 없습니다'}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 404 + + except Exception as e: + logging.error(f"바코드→APC 조회 오류: {e}") + response = jsonify({'success': False, 'error': f'바코드 조회 오류: {str(e)}'}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 500 + + if not apc: + response = jsonify({'success': False, 'error': 'APC 코드가 필요합니다'}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 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 + a.product_name, + a.company_name, + a.main_ingredient, + a.efficacy_effect, + a.dosage_instructions, + a.precautions, + a.weight_min_kg, + a.weight_max_kg, + a.pet_size_label, + a.component_code, + g.component_name_ko, + g.dosing_interval_adult, + g.dosing_interval_high_risk, + g.dosing_interval_puppy, + g.companion_drugs + FROM apc a + LEFT JOIN component_guide g ON a.component_code = g.component_code + WHERE a.apc = :apc + LIMIT 1 + """), {'apc': apc}) + row = result.fetchone() + + # 포장단위 APC → 대표 APC 폴백 + if not row and len(apc) == 13 and apc.startswith('023'): + item_prefix = apc[:8] + result = conn.execute(text(""" + SELECT + a.product_name, a.company_name, a.main_ingredient, + a.efficacy_effect, a.dosage_instructions, a.precautions, + a.weight_min_kg, a.weight_max_kg, a.pet_size_label, + a.component_code, + g.component_name_ko, g.dosing_interval_adult, + g.dosing_interval_high_risk, g.dosing_interval_puppy, + g.companion_drugs + FROM apc a + LEFT JOIN component_guide g ON a.component_code = g.component_code + WHERE a.apc LIKE :prefix + ORDER BY LENGTH(a.apc) + LIMIT 1 + """), {'prefix': f'{item_prefix}%'}) + row = result.fetchone() + + if not row: + response = jsonify({'success': False, 'error': f'APC {apc} 정보를 찾을 수 없습니다'}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 404 + + except Exception as e: + logging.error(f"PostgreSQL 조회 오류: {e}") + response = jsonify({'success': False, 'error': f'DB 조회 오류: {str(e)}'}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 500 + + # HTML 태그 제거 함수 + def strip_html(html_text): + if not html_text: + return '' + text = re.sub(r'

||', '\n', html_text) + text = re.sub(r'<[^>]+>', '', text) + text = unescape(text) + if '─' in text or '━' in text or ('======' in text and '------' in text): + lines = text.split('\n') + cleaned = [line.strip() for line in lines if line.strip()] + return '\n'.join(cleaned) + else: + text = re.sub(r'\s+', ' ', text).strip() + return text + + # 항목별 줄바꿈 처리 + def format_for_print(text): + if not text: + return '' + text = re.sub(r'\s*(가|나|다|라|마|바|사|아|자)\.\s*', r'\n\1. ', text) + text = re.sub(r'\s+(\d+)\)\s*', r'\n \1) ', text) + return text.strip() + + # 텍스트 줄바꿈 + 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 or '' + 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) + + # 80mm 프린터 = 48자 기준 + LINE = "=" * 48 + THIN = "-" * 48 + + message = f""" +{LINE} + [ 애니팜 투약지도서 ] +{LINE} + +{pg_product_name} +""" + if company: + message += f"제조: {company}\n" + + if ingredient and ingredient != 'NaN': + message += f""" +{THIN} +▶ 주성분 +""" + for line in wrap_text(ingredient, 46): + message += f" {line}\n" + + if efficacy: + message += f""" +{THIN} +▶ 효능효과 +""" + formatted_efficacy = format_for_print(efficacy) + for para in formatted_efficacy.split('\n'): + for line in wrap_text(para.strip(), 44): + message += f" {line}\n" + + if dosage: + message += f""" +{THIN} +▶ 용법용량 +""" + has_box_table = '─' in dosage or '━' in dosage + has_ascii_table = '======' in dosage and '------' in dosage + if has_box_table: + for line in dosage.split('\n'): + line = line.strip() + if line: + message += f"{line}\n" + elif has_ascii_table: + for line in dosage.split('\n'): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith('===') or stripped.startswith('---'): + message += f" {'─' * 44}\n" + else: + message += f" {stripped}\n" + else: + formatted_dosage = format_for_print(dosage) + for para in formatted_dosage.split('\n'): + for line in wrap_text(para.strip(), 44): + message += f" {line}\n" + + # 투약 주기 + if row.dosing_interval_adult: + message += f""" +{THIN} +★ 투약 주기 ★ +""" + message += f" 일반: {row.dosing_interval_adult}\n" + if row.dosing_interval_high_risk: + message += f" 고위험: {row.dosing_interval_high_risk}\n" + if row.dosing_interval_puppy: + message += f" 새끼: {row.dosing_interval_puppy}\n" + + # 병용약 권장 + if row.companion_drugs: + message += f""" +{THIN} +★ 함께 투약 권장 ★ +""" + for line in wrap_text(row.companion_drugs, 44): + message += f" {line}\n" + + # 주의사항 + if precautions: + message += f""" +{THIN} +▶ 주의사항 +""" + formatted_precautions = format_for_print(precautions) + for para in formatted_precautions.split('\n'): + for line in wrap_text(para.strip(), 44): + message += f" {line}\n" + + message += f""" +{LINE} + 청 춘 약 국 + Tel: 033-481-5222 +""" + + # 네트워크 프린터로 인쇄 + from pos_printer import print_text + + result = print_text(message, cut=True) + + if result: + response = jsonify({ + 'success': True, + 'message': '동물약 안내서 인쇄 완료', + 'apc': apc, + 'product_name': pg_product_name + }) + response.headers['Access-Control-Allow-Origin'] = '*' + return response + else: + response = jsonify({'success': False, 'error': '프린터 출력 실패'}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 500 + + except Exception as e: + logging.error(f"동물약 GET 인쇄 API 오류: {e}") + import traceback + traceback.print_exc() + response = jsonify({'success': False, 'error': str(e)}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 500 + + +@app.route('/api/animal-drug-print/', methods=['OPTIONS']) +def api_animal_drug_print_options(code): + """CORS preflight 요청 처리""" + response = make_response() + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + return response + + @app.route('/api/animal-drug-info/preview', methods=['POST']) def api_animal_drug_info_preview(): """동물약 정보 미리보기 (텍스트 반환)""" @@ -10386,6 +10703,143 @@ def api_stock_forecast(): return jsonify({'success': False, 'error': str(e)}), 500 +# ══════════════════════════════════════════════════════════════════════════════ +# 바코드로 제품 이미지 조회 API (외부 CORS 지원) +# ══════════════════════════════════════════════════════════════════════════════ + +@app.route('/api/product-image/') +def api_product_image_by_barcode(barcode): + """ + 바코드로 제품 이미지 반환 API + GET /api/product-image/8806265001309 + GET /api/product-image/8806265001309?type=thumbnail + + - CORS 지원 (외부에서 호출 가능) + - type=thumbnail: 썸네일 반환 + - 이미지가 없으면 404 + """ + try: + img_type = request.args.get('type', 'full') # full 또는 thumbnail + + img_db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db') + conn = sqlite3.connect(img_db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + if img_type == 'thumbnail': + cursor.execute(""" + SELECT thumbnail_base64, product_name + FROM product_images + WHERE barcode = ? AND thumbnail_base64 IS NOT NULL + """, (barcode,)) + col_name = 'thumbnail_base64' + else: + cursor.execute(""" + SELECT image_base64, product_name + FROM product_images + WHERE barcode = ? AND image_base64 IS NOT NULL + """, (barcode,)) + col_name = 'image_base64' + + row = cursor.fetchone() + conn.close() + + if not row or not row[col_name]: + response = jsonify({'success': False, 'error': 'Image not found'}) + response.status_code = 404 + response.headers['Access-Control-Allow-Origin'] = '*' + return response + + # base64 디코딩 + import base64 + image_data = base64.b64decode(row[col_name]) + + # 이미지 응답 생성 + response = make_response(image_data) + response.headers['Content-Type'] = 'image/jpeg' + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS' + response.headers['Cache-Control'] = 'public, max-age=86400' # 24시간 캐시 + response.headers['X-Product-Name'] = row['product_name'].encode('utf-8').decode('latin-1') if row['product_name'] else '' + + return response + + except Exception as e: + logging.error(f"product-image API 오류: {e}") + response = jsonify({'success': False, 'error': str(e)}) + response.status_code = 500 + response.headers['Access-Control-Allow-Origin'] = '*' + return response + + +@app.route('/api/product-image/', methods=['OPTIONS']) +def api_product_image_options(barcode): + """CORS preflight 요청 처리""" + response = make_response() + response.headers['Access-Control-Allow-Origin'] = '*' + response.headers['Access-Control-Allow-Methods'] = 'GET, OPTIONS' + response.headers['Access-Control-Allow-Headers'] = 'Content-Type' + return response + + +@app.route('/api/product-image-info/') +def api_product_image_info(barcode): + """ + 바코드로 제품 이미지 정보 조회 (JSON) + GET /api/product-image-info/8806265001309 + + 이미지 존재 여부, 제품명, URL 등 메타데이터 반환 + """ + try: + img_db_path = os.path.join(os.path.dirname(__file__), 'db', 'product_images.db') + conn = sqlite3.connect(img_db_path) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute(""" + SELECT barcode, drug_code, product_name, image_url, source, status, + CASE WHEN image_base64 IS NOT NULL THEN 1 ELSE 0 END as has_image, + CASE WHEN thumbnail_base64 IS NOT NULL THEN 1 ELSE 0 END as has_thumbnail, + created_at, updated_at + FROM product_images + WHERE barcode = ? + """, (barcode,)) + + row = cursor.fetchone() + conn.close() + + if not row: + response = jsonify({'success': False, 'error': 'Product not found'}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 404 + + response = jsonify({ + 'success': True, + 'data': { + 'barcode': row['barcode'], + 'drug_code': row['drug_code'], + 'product_name': row['product_name'], + 'image_url': f"/api/product-image/{row['barcode']}", + 'thumbnail_url': f"/api/product-image/{row['barcode']}?type=thumbnail", + 'original_url': row['image_url'], + 'source': row['source'], + 'status': row['status'], + 'has_image': bool(row['has_image']), + 'has_thumbnail': bool(row['has_thumbnail']), + 'created_at': row['created_at'], + 'updated_at': row['updated_at'] + } + }) + response.headers['Access-Control-Allow-Origin'] = '*' + return response + + except Exception as e: + logging.error(f"product-image-info API 오류: {e}") + response = jsonify({'success': False, 'error': str(e)}) + response.headers['Access-Control-Allow-Origin'] = '*' + return response, 500 + + if __name__ == '__main__': import os