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'
', 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