feat: 동물약 안내서/제품 이미지 외부 API 추가 (CORS 지원)

- GET /api/animal-drug-print/{apc|barcode} - 바로 인쇄
- GET /api/product-image/{barcode} - 제품 이미지 반환
- GET /api/product-image-info/{barcode} - 이미지 메타데이터
- 바코드→APC 자동 변환 지원
This commit is contained in:
thug0bin
2026-03-14 00:18:04 +09:00
parent aed0c314b7
commit 67fb7bf937

View File

@@ -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/<code>')
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'</p>|<br\s*/?>|</div>', '\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/<code>', 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/<barcode>')
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/<barcode>', 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/<barcode>')
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