feat: 제품 검색 페이지 및 QR 라벨 인쇄 기능

- /admin/products: 전체 제품 검색 페이지 (OTC)
- /api/products: 제품 검색 API (세트상품 바코드 포함)
- qr_printer.py: Brother QL-710W 프린터 연동
- /api/qr-print, /api/qr-preview: QR 라벨 인쇄/미리보기 API
- 판매상세 페이지에 QR 인쇄 버튼 추가
- 수량 선택 UI (+/- 버튼, 최대 10장)
- 세트상품 제조사 표시 개선
- 대시보드 헤더에 제품검색/판매조회 탭 추가
This commit is contained in:
thug0bin
2026-02-27 13:56:26 +09:00
parent f3fa4707ac
commit 9bd2174501
11 changed files with 1950 additions and 8 deletions

View File

@@ -2461,6 +2461,93 @@ def admin_sales_detail():
return render_template('admin_sales_detail.html')
# ===== 제품 검색 페이지 =====
@app.route('/admin/products')
def admin_products():
"""제품 검색 페이지 (전체 재고에서 검색, QR 인쇄)"""
return render_template('admin_products.html')
@app.route('/api/products')
def api_products():
"""
제품 검색 API
- 상품명, 상품코드, 바코드로 검색
- 세트상품 바코드도 CD_ITEM_UNIT_MEMBER에서 조회
"""
search = request.args.get('search', '').strip()
limit = int(request.args.get('limit', 100))
if not search or len(search) < 2:
return jsonify({'success': False, 'error': '검색어는 2글자 이상 입력하세요'})
try:
drug_session = db_manager.get_session('PM_DRUG')
# 제품 검색 쿼리
# CD_GOODS.BARCODE가 없으면 CD_ITEM_UNIT_MEMBER.CD_CD_BARCODE 사용 (세트상품)
# 세트상품 여부 확인: CD_item_set에 SetCode로 존재하면 세트상품
products_query = text(f"""
SELECT TOP {limit}
G.DrugCode as drug_code,
G.GoodsName as product_name,
COALESCE(NULLIF(G.BARCODE, ''), U.CD_CD_BARCODE, '') as barcode,
G.Saleprice as sale_price,
G.Price as cost_price,
CASE
WHEN G.SplName IS NOT NULL AND G.SplName != '' THEN G.SplName
WHEN SET_CHK.is_set = 1 THEN N'세트상품'
ELSE ''
END as supplier,
CASE WHEN SET_CHK.is_set = 1 THEN 1 ELSE 0 END as is_set
FROM CD_GOODS G
OUTER APPLY (
SELECT TOP 1 CD_CD_BARCODE
FROM CD_ITEM_UNIT_MEMBER
WHERE DRUGCODE = G.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != ''
) U
OUTER APPLY (
SELECT TOP 1 1 as is_set
FROM CD_item_set
WHERE SetCode = G.DrugCode AND DrugCode = 'SET0000'
) SET_CHK
WHERE
G.GoodsName LIKE :search_like
OR G.DrugCode LIKE :search_like
OR G.BARCODE LIKE :search_like
OR U.CD_CD_BARCODE LIKE :search_like
ORDER BY G.GoodsName
""")
search_like = f'%{search}%'
rows = drug_session.execute(products_query, {
'search_like': search_like
}).fetchall()
items = []
for row in rows:
items.append({
'drug_code': row.drug_code or '',
'product_name': row.product_name or '',
'barcode': row.barcode or '',
'sale_price': float(row.sale_price) if row.sale_price else 0,
'cost_price': float(row.cost_price) if row.cost_price else 0,
'supplier': row.supplier or '',
'is_set': bool(row.is_set)
})
return jsonify({
'success': True,
'items': items,
'count': len(items)
})
except Exception as e:
logging.error(f"제품 검색 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/admin/sales')
def admin_sales_pos():
"""판매 내역 페이지 (POS 스타일, 거래별 그룹핑)"""
@@ -2483,6 +2570,7 @@ def api_sales_detail():
# 판매 내역 조회 (최근 N일)
# CD_GOODS.BARCODE가 없으면 CD_ITEM_UNIT_MEMBER.CD_CD_BARCODE 사용 (세트상품/자체등록 바코드)
# 세트상품 여부 확인: CD_item_set에 SetCode로 존재하면 세트상품
sales_query = text("""
SELECT
S.SL_DT_appl as sale_date,
@@ -2490,7 +2578,11 @@ def api_sales_detail():
S.DrugCode as drug_code,
ISNULL(G.GoodsName, '알 수 없음') as product_name,
COALESCE(NULLIF(G.BARCODE, ''), U.CD_CD_BARCODE, '') as barcode,
ISNULL(G.SplName, '') as supplier,
CASE
WHEN G.SplName IS NOT NULL AND G.SplName != '' THEN G.SplName
WHEN SET_CHK.is_set = 1 THEN '세트상품'
ELSE ''
END as supplier,
ISNULL(S.QUAN, 1) as quantity,
ISNULL(S.SL_TOTAL_PRICE, 0) as total_price_db,
ISNULL(G.Saleprice, 0) as unit_price
@@ -2501,6 +2593,11 @@ def api_sales_detail():
FROM PM_DRUG.dbo.CD_ITEM_UNIT_MEMBER
WHERE DRUGCODE = S.DrugCode AND CD_CD_BARCODE IS NOT NULL AND CD_CD_BARCODE != ''
) U
OUTER APPLY (
SELECT TOP 1 1 as is_set
FROM PM_DRUG.dbo.CD_item_set
WHERE SetCode = S.DrugCode AND DrugCode = 'SET0000'
) SET_CHK
WHERE S.SL_DT_appl >= CONVERT(VARCHAR(8), DATEADD(DAY, -:days, GETDATE()), 112)
ORDER BY S.SL_DT_appl DESC, S.SL_NO_order DESC
""")
@@ -2745,6 +2842,84 @@ def api_claude_status():
}), 500
# =============================================================================
# QR 라벨 인쇄 API
# =============================================================================
@app.route('/api/qr-print', methods=['POST'])
def api_qr_print():
"""QR 라벨 인쇄 API"""
try:
from qr_printer import print_drug_qr_label
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '데이터가 없습니다'}), 400
drug_name = data.get('drug_name', '')
barcode = data.get('barcode', '')
drug_code = data.get('drug_code', '')
sale_price = data.get('sale_price', 0)
if not drug_name:
return jsonify({'success': False, 'error': '상품명이 필요합니다'}), 400
# 바코드가 없으면 drug_code 사용
qr_data = barcode if barcode else drug_code
result = print_drug_qr_label(
drug_name=drug_name,
barcode=qr_data,
sale_price=sale_price,
drug_code=drug_code,
pharmacy_name='청춘약국'
)
return jsonify(result)
except ImportError as e:
logging.error(f"QR 프린터 모듈 로드 실패: {e}")
return jsonify({'success': False, 'error': 'QR 프린터 모듈이 없습니다'}), 500
except Exception as e:
logging.error(f"QR 인쇄 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
@app.route('/api/qr-preview', methods=['POST'])
def api_qr_preview():
"""QR 라벨 미리보기 API (base64 이미지 반환)"""
try:
from qr_printer import preview_qr_label
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': '데이터가 없습니다'}), 400
drug_name = data.get('drug_name', '')
barcode = data.get('barcode', '')
drug_code = data.get('drug_code', '')
sale_price = data.get('sale_price', 0)
if not drug_name:
return jsonify({'success': False, 'error': '상품명이 필요합니다'}), 400
qr_data = barcode if barcode else drug_code
image_data = preview_qr_label(
drug_name=drug_name,
barcode=qr_data,
sale_price=sale_price,
drug_code=drug_code,
pharmacy_name='청춘약국'
)
return jsonify({'success': True, 'image': image_data})
except Exception as e:
logging.error(f"QR 미리보기 오류: {e}")
return jsonify({'success': False, 'error': str(e)}), 500
if __name__ == '__main__':
# 개발 모드로 실행
app.run(host='0.0.0.0', port=7001, debug=True)