feat: OTC 용법 라벨 시스템 구현

DB:
- otc_label_presets 테이블 추가 (SQLite)
- 바코드 기준 오버라이드 데이터 저장

Backend:
- utils/otc_label_printer.py: 라벨 이미지 생성 + Brother QL-810W 출력
- API: CRUD + 미리보기 렌더링 + MSSQL 약품 검색

Frontend:
- /admin/otc-labels: 관리 페이지
- 실시간 미리보기
- 저장된 프리셋 목록
- 바코드/이름 검색 → 프리셋 편집 → 인쇄
This commit is contained in:
thug0bin
2026-03-02 17:00:47 +09:00
parent c525632246
commit 76a4280ebd
5 changed files with 1215 additions and 0 deletions

View File

@@ -5375,6 +5375,267 @@ def api_admin_qr_print():
return jsonify({'success': False, 'error': str(e)}), 500
# ============================================================================
# OTC 용법 라벨 시스템 API
# ============================================================================
@app.route('/admin/otc-labels')
def admin_otc_labels():
"""OTC 용법 라벨 관리 페이지"""
return render_template('admin_otc_labels.html')
@app.route('/api/admin/otc-labels', methods=['GET'])
def api_get_otc_labels():
"""OTC 라벨 프리셋 목록 조회"""
try:
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT id, barcode, drug_code, display_name, effect,
dosage_instruction, usage_tip, use_wide_format,
print_count, last_printed_at, created_at, updated_at
FROM otc_label_presets
ORDER BY updated_at DESC
""")
rows = cursor.fetchall()
labels = [dict(row) for row in rows]
return jsonify({
'success': True,
'count': len(labels),
'labels': labels
})
except Exception as e:
logging.error(f"OTC 라벨 목록 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/otc-labels/<barcode>', methods=['GET'])
def api_get_otc_label(barcode):
"""OTC 라벨 프리셋 단건 조회 (바코드 기준)"""
try:
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT id, barcode, drug_code, display_name, effect,
dosage_instruction, usage_tip, use_wide_format,
print_count, last_printed_at, created_at, updated_at
FROM otc_label_presets
WHERE barcode = ?
""", (barcode,))
row = cursor.fetchone()
if not row:
return jsonify({'success': False, 'error': '등록된 프리셋이 없습니다.', 'exists': False}), 404
return jsonify({
'success': True,
'exists': True,
'label': dict(row)
})
except Exception as e:
logging.error(f"OTC 라벨 조회 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/otc-labels', methods=['POST'])
def api_upsert_otc_label():
"""OTC 라벨 프리셋 등록/수정 (Upsert)"""
try:
data = request.get_json()
if not data or not data.get('barcode'):
return jsonify({'success': False, 'error': 'barcode는 필수입니다.'}), 400
barcode = data['barcode']
drug_code = data.get('drug_code', '')
display_name = data.get('display_name', '')
effect = data.get('effect', '')
dosage_instruction = data.get('dosage_instruction', '')
usage_tip = data.get('usage_tip', '')
use_wide_format = data.get('use_wide_format', True)
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
# Upsert (INSERT OR REPLACE)
cursor.execute("""
INSERT INTO otc_label_presets
(barcode, drug_code, display_name, effect, dosage_instruction, usage_tip, use_wide_format, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(barcode) DO UPDATE SET
drug_code = excluded.drug_code,
display_name = excluded.display_name,
effect = excluded.effect,
dosage_instruction = excluded.dosage_instruction,
usage_tip = excluded.usage_tip,
use_wide_format = excluded.use_wide_format,
updated_at = CURRENT_TIMESTAMP
""", (barcode, drug_code, display_name, effect, dosage_instruction, usage_tip, use_wide_format))
conn.commit()
return jsonify({
'success': True,
'message': f'라벨 프리셋 저장 완료 ({barcode})'
})
except Exception as e:
logging.error(f"OTC 라벨 저장 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/otc-labels/<barcode>', methods=['DELETE'])
def api_delete_otc_label(barcode):
"""OTC 라벨 프리셋 삭제"""
try:
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM otc_label_presets WHERE barcode = ?", (barcode,))
conn.commit()
if cursor.rowcount > 0:
return jsonify({'success': True, 'message': f'삭제 완료 ({barcode})'})
else:
return jsonify({'success': False, 'error': '존재하지 않는 프리셋'}), 404
except Exception as e:
logging.error(f"OTC 라벨 삭제 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/otc-labels/preview', methods=['POST'])
def api_preview_otc_label():
"""OTC 라벨 미리보기 이미지 생성"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '요청 데이터가 없습니다.'}), 400
drug_name = data.get('drug_name', '약품명')
effect = data.get('effect', '')
dosage_instruction = data.get('dosage_instruction', '')
usage_tip = data.get('usage_tip', '')
from utils.otc_label_printer import generate_preview_image
preview_url = generate_preview_image(drug_name, effect, dosage_instruction, usage_tip)
if preview_url:
return jsonify({
'success': True,
'preview_url': preview_url
})
else:
return jsonify({'success': False, 'error': '미리보기 생성 실패'}), 500
except Exception as e:
logging.error(f"OTC 라벨 미리보기 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/otc-labels/print', methods=['POST'])
def api_print_otc_label():
"""OTC 라벨 인쇄 (Brother QL-810W)"""
try:
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '요청 데이터가 없습니다.'}), 400
barcode = data.get('barcode')
drug_name = data.get('drug_name', '약품명')
effect = data.get('effect', '')
dosage_instruction = data.get('dosage_instruction', '')
usage_tip = data.get('usage_tip', '')
from utils.otc_label_printer import print_otc_label
success = print_otc_label(drug_name, effect, dosage_instruction, usage_tip)
if success:
# 인쇄 횟수 업데이트
if barcode:
conn = db_manager.get_sqlite_connection()
cursor = conn.cursor()
cursor.execute("""
UPDATE otc_label_presets
SET print_count = print_count + 1, last_printed_at = CURRENT_TIMESTAMP
WHERE barcode = ?
""", (barcode,))
conn.commit()
return jsonify({
'success': True,
'message': f'라벨 인쇄 완료: {drug_name}'
})
else:
return jsonify({'success': False, 'error': '프린터 전송 실패'}), 500
except Exception as e:
logging.error(f"OTC 라벨 인쇄 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/admin/otc-labels/search-mssql', methods=['GET'])
def api_search_mssql_drug():
"""MSSQL에서 약품 검색 (바코드 또는 이름)"""
try:
query = request.args.get('q', '').strip()
if not query:
return jsonify({'success': False, 'error': '검색어를 입력해주세요.'}), 400
mssql_session = db_manager.get_session('PM_DRUG')
# 바코드 또는 이름으로 검색
sql = text("""
SELECT TOP 20
DrugCode, Barcode, GoodsName, Saleprice, StockQty
FROM CD_GOODS
WHERE (Barcode LIKE :query OR GoodsName LIKE :query)
AND Barcode IS NOT NULL
AND Barcode != ''
ORDER BY
CASE WHEN Barcode = :exact THEN 0 ELSE 1 END,
GoodsName
""")
rows = mssql_session.execute(sql, {
'query': f'%{query}%',
'exact': query
}).fetchall()
drugs = []
for row in rows:
drugs.append({
'drug_code': row.DrugCode,
'barcode': row.Barcode,
'goods_name': row.GoodsName,
'sale_price': float(row.Saleprice or 0),
'stock_qty': int(row.StockQty or 0)
})
return jsonify({
'success': True,
'count': len(drugs),
'drugs': drugs
})
except Exception as e:
logging.error(f"MSSQL 약품 검색 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
if __name__ == '__main__':
import os