Compare commits
4 Commits
aed0c314b7
...
3871154509
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3871154509 | ||
|
|
f855fc5916 | ||
|
|
cb7450f654 | ||
|
|
67fb7bf937 |
629
backend/app.py
629
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
|
||||
@@ -7846,6 +7846,179 @@ def api_print_otc_label():
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# OTC 라벨 바로 인쇄 API (GET - URL로 바로 인쇄) - 외부 CORS 지원
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@app.route('/api/otc-label-print/<barcode>')
|
||||
def api_otc_label_print_by_barcode(barcode):
|
||||
"""
|
||||
OTC 라벨 바로 인쇄 (GET 방식)
|
||||
GET /api/otc-label-print/8806436044814
|
||||
|
||||
- CORS 지원 (외부에서 호출 가능)
|
||||
- 저장된 프리셋이 있으면 해당 데이터로 인쇄
|
||||
- 프리셋이 없으면 MSSQL에서 약품명 조회 후 기본 라벨 인쇄
|
||||
"""
|
||||
try:
|
||||
if not OTC_LABEL_AVAILABLE:
|
||||
response = jsonify({'success': False, 'error': 'OTC 라벨 모듈이 로드되지 않았습니다.'})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response, 500
|
||||
|
||||
# 1. 프리셋 조회
|
||||
conn = db_manager.get_sqlite_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
SELECT display_name, effect, dosage_instruction, usage_tip
|
||||
FROM otc_label_presets
|
||||
WHERE barcode = ?
|
||||
""", (barcode,))
|
||||
|
||||
preset = cursor.fetchone()
|
||||
|
||||
if preset:
|
||||
# 프리셋 있음 → 프리셋 데이터로 인쇄
|
||||
drug_name = preset['display_name']
|
||||
effect = preset['effect'] or ''
|
||||
dosage_instruction = preset['dosage_instruction'] or ''
|
||||
usage_tip = preset['usage_tip'] or ''
|
||||
else:
|
||||
# 프리셋 없음 → 인쇄 안 함
|
||||
response = jsonify({
|
||||
'success': False,
|
||||
'error': f'바코드 {barcode}에 등록된 라벨 프리셋이 없습니다.',
|
||||
'barcode': barcode,
|
||||
'has_preset': False
|
||||
})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response, 404
|
||||
|
||||
# 2. 인쇄 실행
|
||||
success = print_otc_label(drug_name, effect, dosage_instruction, usage_tip)
|
||||
|
||||
if success:
|
||||
# 인쇄 횟수 업데이트 (프리셋 있는 경우)
|
||||
if preset:
|
||||
cursor.execute("""
|
||||
UPDATE otc_label_presets
|
||||
SET print_count = print_count + 1, last_printed_at = CURRENT_TIMESTAMP
|
||||
WHERE barcode = ?
|
||||
""", (barcode,))
|
||||
conn.commit()
|
||||
|
||||
response = jsonify({
|
||||
'success': True,
|
||||
'message': f'OTC 라벨 인쇄 완료: {drug_name}',
|
||||
'barcode': barcode,
|
||||
'drug_name': drug_name,
|
||||
'has_preset': preset is not None
|
||||
})
|
||||
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"OTC 라벨 GET 인쇄 오류: {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/otc-label-print/<barcode>', methods=['OPTIONS'])
|
||||
def api_otc_label_print_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/otc-label-check', methods=['GET', 'POST'])
|
||||
def api_otc_label_check():
|
||||
"""
|
||||
OTC 라벨 프리셋 존재 여부 일괄 확인 API
|
||||
|
||||
GET /api/otc-label-check?barcodes=8806436044814,8806436058613
|
||||
POST /api/otc-label-check {"barcodes": ["8806436044814", "8806436058613"]}
|
||||
|
||||
- CORS 지원 (외부에서 호출 가능)
|
||||
- 장바구니 등에서 인쇄 버튼 활성화 여부 판단용
|
||||
"""
|
||||
try:
|
||||
# 바코드 목록 파싱
|
||||
if request.method == 'POST':
|
||||
data = request.get_json() or {}
|
||||
barcodes = data.get('barcodes', [])
|
||||
else:
|
||||
barcodes_param = request.args.get('barcodes', '')
|
||||
barcodes = [b.strip() for b in barcodes_param.split(',') if b.strip()]
|
||||
|
||||
if not barcodes:
|
||||
response = jsonify({'success': False, 'error': '바코드 목록이 필요합니다'})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response, 400
|
||||
|
||||
# 프리셋 존재 여부 확인
|
||||
conn = db_manager.get_sqlite_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
placeholders = ','.join(['?' for _ in barcodes])
|
||||
cursor.execute(f"""
|
||||
SELECT barcode, display_name
|
||||
FROM otc_label_presets
|
||||
WHERE barcode IN ({placeholders})
|
||||
""", barcodes)
|
||||
|
||||
preset_map = {row['barcode']: row['display_name'] for row in cursor.fetchall()}
|
||||
|
||||
# 결과 생성
|
||||
results = []
|
||||
printable_count = 0
|
||||
for barcode in barcodes:
|
||||
has_preset = barcode in preset_map
|
||||
if has_preset:
|
||||
printable_count += 1
|
||||
results.append({
|
||||
'barcode': barcode,
|
||||
'has_preset': has_preset,
|
||||
'display_name': preset_map.get(barcode),
|
||||
'printable': has_preset
|
||||
})
|
||||
|
||||
response = jsonify({
|
||||
'success': True,
|
||||
'total': len(barcodes),
|
||||
'printable_count': printable_count,
|
||||
'results': results
|
||||
})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"OTC 라벨 체크 오류: {e}")
|
||||
response = jsonify({'success': False, 'error': str(e)})
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
return response, 500
|
||||
|
||||
|
||||
@app.route('/api/otc-label-check', methods=['OPTIONS'])
|
||||
def api_otc_label_check_options():
|
||||
"""CORS preflight 요청 처리"""
|
||||
response = make_response()
|
||||
response.headers['Access-Control-Allow-Origin'] = '*'
|
||||
response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS'
|
||||
response.headers['Access-Control-Allow-Headers'] = 'Content-Type'
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/api/admin/otc-labels/search-mssql', methods=['GET'])
|
||||
def api_search_mssql_drug():
|
||||
"""MSSQL에서 약품 검색 (바코드 또는 이름)"""
|
||||
@@ -8535,6 +8708,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 +10876,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
|
||||
|
||||
|
||||
@@ -25,23 +25,29 @@ import websockets
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Gateway 설정 (clawdbot.json에서 읽기)
|
||||
# Gateway 설정 (openclaw.json 또는 clawdbot.json에서 읽기)
|
||||
OPENCLAW_CONFIG_PATH = Path.home() / '.openclaw' / 'openclaw.json'
|
||||
CLAWDBOT_CONFIG_PATH = Path.home() / '.clawdbot' / 'clawdbot.json'
|
||||
|
||||
|
||||
def _load_gateway_config():
|
||||
"""clawdbot.json에서 Gateway 설정 로드"""
|
||||
try:
|
||||
with open(CLAWDBOT_CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
gw = config.get('gateway', {})
|
||||
return {
|
||||
'port': gw.get('port', 18789),
|
||||
'token': gw.get('auth', {}).get('token', ''),
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"[Clawdbot] 설정 파일 로드 실패: {e}")
|
||||
return {'port': 18789, 'token': ''}
|
||||
"""OpenClaw/Clawdbot Gateway 설정 로드"""
|
||||
for config_path in [OPENCLAW_CONFIG_PATH, CLAWDBOT_CONFIG_PATH]:
|
||||
try:
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
gw = config.get('gateway', {})
|
||||
token = gw.get('auth', {}).get('token', '')
|
||||
if token:
|
||||
logger.info(f"[Gateway] 설정 로드: {config_path.name}")
|
||||
return {
|
||||
'port': gw.get('port', 18789),
|
||||
'token': token,
|
||||
}
|
||||
except Exception:
|
||||
continue
|
||||
logger.warning("[Gateway] 설정 파일 로드 실패")
|
||||
return {'port': 18789, 'token': ''}
|
||||
|
||||
|
||||
async def _ask_gateway(message, session_id='pharmacy-upsell',
|
||||
@@ -85,10 +91,10 @@ async def _ask_gateway(message, session_id='pharmacy-upsell',
|
||||
'maxProtocol': 3,
|
||||
'client': {
|
||||
'id': 'gateway-client',
|
||||
'displayName': 'Pharmacy Upsell',
|
||||
'displayName': 'Pharmacy PAAI',
|
||||
'version': '1.0.0',
|
||||
'platform': 'win32',
|
||||
'mode': 'backend',
|
||||
'mode': 'cli',
|
||||
'instanceId': str(uuid.uuid4()),
|
||||
},
|
||||
'caps': [],
|
||||
@@ -96,7 +102,7 @@ async def _ask_gateway(message, session_id='pharmacy-upsell',
|
||||
'token': token,
|
||||
},
|
||||
'role': 'operator',
|
||||
'scopes': ['operator.admin'],
|
||||
'scopes': ['operator.admin', 'operator.write', 'operator.read'],
|
||||
}
|
||||
}
|
||||
await ws.send(json.dumps(connect_frame))
|
||||
@@ -198,27 +204,65 @@ async def _ask_gateway(message, session_id='pharmacy-upsell',
|
||||
def ask_clawdbot(message, session_id='pharmacy-upsell',
|
||||
system_prompt=None, timeout=60, model=None):
|
||||
"""
|
||||
동기 래퍼: Flask에서 직접 호출 가능
|
||||
|
||||
OpenClaw CLI를 통한 AI 호출 (WebSocket 대신)
|
||||
|
||||
Args:
|
||||
message: 사용자 메시지
|
||||
session_id: 세션 ID (대화 구분용)
|
||||
system_prompt: 추가 시스템 프롬프트
|
||||
system_prompt: 추가 시스템 프롬프트 (현재 미사용)
|
||||
timeout: 타임아웃 (초)
|
||||
model: 모델 오버라이드 (예: 'anthropic/claude-sonnet-4-5')
|
||||
model: 모델 오버라이드 (현재 미사용 - CLI가 기본 모델 사용)
|
||||
|
||||
Returns:
|
||||
str: AI 응답 텍스트 (실패 시 None)
|
||||
"""
|
||||
import subprocess
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
loop = asyncio.new_event_loop()
|
||||
result = loop.run_until_complete(
|
||||
_ask_gateway(message, session_id, system_prompt, timeout, model=model)
|
||||
# Node.js로 OpenClaw 직접 호출 (shell 없이, 특수문자 안전)
|
||||
node_path = r'C:\Program Files\nodejs\node.exe'
|
||||
openclaw_path = str(Path.home() / 'AppData/Roaming/npm/node_modules/openclaw/openclaw.mjs')
|
||||
|
||||
cmd = [node_path, openclaw_path, 'agent', '-m', message, '--session-id', session_id, '--json']
|
||||
logger.info(f"[OpenClaw] session={session_id}, msg_len={len(message)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout + 30,
|
||||
encoding='utf-8',
|
||||
env={**os.environ, 'PYTHONIOENCODING': 'utf-8'}
|
||||
)
|
||||
loop.close()
|
||||
return result
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.warning(f"[OpenClaw] CLI 에러: {result.stderr}")
|
||||
# 에러가 있어도 stdout에 결과가 있을 수 있음
|
||||
if not result.stdout:
|
||||
return None
|
||||
|
||||
# JSON 파싱
|
||||
data = json.loads(result.stdout)
|
||||
if data.get('status') == 'ok':
|
||||
payloads = data.get('result', {}).get('payloads', [])
|
||||
if payloads:
|
||||
text = payloads[0].get('text', '')
|
||||
logger.info(f"[OpenClaw] 응답 수신: {len(text)}자")
|
||||
return text
|
||||
|
||||
logger.warning(f"[OpenClaw] 응답 없음: {data}")
|
||||
return None
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning(f"[OpenClaw] 타임아웃 ({timeout}초)")
|
||||
return None
|
||||
except json.JSONDecodeError as e:
|
||||
logger.warning(f"[OpenClaw] JSON 파싱 실패: {e}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warning(f"[Clawdbot] 호출 실패: {e}")
|
||||
logger.warning(f"[OpenClaw] 호출 실패: {e}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -356,13 +400,13 @@ async def _get_gateway_status():
|
||||
'displayName': 'Pharmacy Status',
|
||||
'version': '1.0.0',
|
||||
'platform': 'win32',
|
||||
'mode': 'backend',
|
||||
'mode': 'cli',
|
||||
'instanceId': str(uuid.uuid4()),
|
||||
},
|
||||
'caps': [],
|
||||
'auth': {'token': token},
|
||||
'role': 'operator',
|
||||
'scopes': ['operator.read'],
|
||||
'scopes': ['operator.admin', 'operator.write', 'operator.read'],
|
||||
}
|
||||
}
|
||||
await ws.send(json.dumps(connect_frame))
|
||||
|
||||
Reference in New Issue
Block a user