- /admin/products: 전체 제품 검색 페이지 (OTC) - /api/products: 제품 검색 API (세트상품 바코드 포함) - qr_printer.py: Brother QL-710W 프린터 연동 - /api/qr-print, /api/qr-preview: QR 라벨 인쇄/미리보기 API - 판매상세 페이지에 QR 인쇄 버튼 추가 - 수량 선택 UI (+/- 버튼, 최대 10장) - 세트상품 제조사 표시 개선 - 대시보드 헤더에 제품검색/판매조회 탭 추가
263 lines
8.2 KiB
Python
263 lines
8.2 KiB
Python
# qr_printer.py - Brother QL-710W QR 라벨 인쇄
|
|
# person-lookup-web-local/print_label.py에서 핵심 기능만 추출
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
import io
|
|
import logging
|
|
import qrcode
|
|
|
|
# 프린터 설정
|
|
PRINTER_IP = "192.168.0.121"
|
|
PRINTER_MODEL = "QL-710W"
|
|
LABEL_TYPE = "29" # 29mm 연속 출력 용지
|
|
|
|
# Windows 폰트 경로
|
|
FONT_PATH = "C:/Windows/Fonts/malgunbd.ttf"
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
def create_drug_qr_label(drug_name, barcode, sale_price, drug_code=None, pharmacy_name='청춘약국'):
|
|
"""
|
|
약품 QR 라벨 이미지 생성
|
|
|
|
Parameters:
|
|
drug_name (str): 약품명
|
|
barcode (str): 바코드 (QR 코드로 변환)
|
|
sale_price (float): 판매가격
|
|
drug_code (str, optional): 약품 코드 (바코드가 없을 때 대체)
|
|
pharmacy_name (str, optional): 약국 이름
|
|
|
|
Returns:
|
|
PIL.Image: 생성된 라벨 이미지
|
|
"""
|
|
label_width = 306
|
|
label_height = 380
|
|
image = Image.new("1", (label_width, label_height), "white")
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
# 폰트 설정
|
|
try:
|
|
drug_name_font = ImageFont.truetype(FONT_PATH, 32)
|
|
price_font = ImageFont.truetype(FONT_PATH, 36)
|
|
label_font = ImageFont.truetype(FONT_PATH, 24)
|
|
except IOError:
|
|
drug_name_font = ImageFont.load_default()
|
|
price_font = ImageFont.load_default()
|
|
label_font = ImageFont.load_default()
|
|
logging.warning("폰트 로드 실패. 기본 폰트 사용.")
|
|
|
|
# 바코드가 없으면 약품 코드 사용
|
|
qr_data = barcode if barcode else (drug_code if drug_code else "NO_BARCODE")
|
|
|
|
# QR 코드 생성
|
|
qr = qrcode.QRCode(
|
|
version=1,
|
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
box_size=4,
|
|
border=1,
|
|
)
|
|
qr.add_data(qr_data)
|
|
qr.make(fit=True)
|
|
qr_img = qr.make_image(fill_color="black", back_color="white")
|
|
|
|
# QR 코드 크기 조정 및 배치
|
|
qr_size = 130
|
|
qr_img = qr_img.resize((qr_size, qr_size), Image.LANCZOS)
|
|
qr_x = (label_width - qr_size) // 2
|
|
qr_y = 15
|
|
|
|
if qr_img.mode != '1':
|
|
qr_img = qr_img.convert('1')
|
|
image.paste(qr_img, (qr_x, qr_y))
|
|
|
|
# 약품명 (QR 코드 아래)
|
|
y_position = qr_y + qr_size + 10
|
|
|
|
def draw_wrapped_text(draw, text, y, font, max_width):
|
|
"""텍스트를 여러 줄로 표시"""
|
|
chars = list(text)
|
|
lines = []
|
|
current_line = ""
|
|
|
|
for char in chars:
|
|
test_line = current_line + char
|
|
bbox = draw.textbbox((0, 0), test_line, font=font)
|
|
w = bbox[2] - bbox[0]
|
|
|
|
if w <= max_width:
|
|
current_line = test_line
|
|
else:
|
|
if current_line:
|
|
lines.append(current_line)
|
|
current_line = char
|
|
|
|
if current_line:
|
|
lines.append(current_line)
|
|
|
|
lines = lines[:2] # 최대 2줄
|
|
|
|
for line in lines:
|
|
bbox = draw.textbbox((0, 0), line, font=font)
|
|
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
draw.text(((label_width - w) / 2, y), line, font=font, fill="black")
|
|
y += h + 5
|
|
|
|
return y
|
|
|
|
y_position = draw_wrapped_text(draw, drug_name, y_position, drug_name_font, label_width - 40)
|
|
y_position += 8
|
|
|
|
# 가격
|
|
if sale_price and sale_price > 0:
|
|
price_text = f"₩{int(sale_price):,}"
|
|
else:
|
|
price_text = "가격 미정"
|
|
|
|
bbox = draw.textbbox((0, 0), price_text, font=price_font)
|
|
w, h = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
draw.text(((label_width - w) / 2, y_position), price_text, font=price_font, fill="black")
|
|
y_position += h + 15
|
|
|
|
# 구분선
|
|
line_margin = 30
|
|
draw.line([(line_margin, y_position), (label_width - line_margin, y_position)], fill="black", width=2)
|
|
y_position += 20
|
|
|
|
# 약국 이름
|
|
signature_text = " ".join(pharmacy_name)
|
|
bbox = draw.textbbox((0, 0), signature_text, font=label_font)
|
|
w_sig, h_sig = bbox[2] - bbox[0], bbox[3] - bbox[1]
|
|
|
|
padding = 10
|
|
box_x = (label_width - w_sig) / 2 - padding
|
|
box_y = y_position
|
|
box_x2 = box_x + w_sig + 2 * padding
|
|
box_y2 = box_y + h_sig + 2 * padding
|
|
draw.rectangle([(box_x, box_y), (box_x2, box_y2)], outline="black", width=2)
|
|
draw.text(((label_width - w_sig) / 2, box_y + padding), signature_text, font=label_font, fill="black")
|
|
|
|
# 절취선 테두리
|
|
draw_scissor_border(draw, label_width, label_height)
|
|
|
|
return image
|
|
|
|
|
|
def draw_scissor_border(draw, width, height, edge_size=10, steps=20):
|
|
"""절취선 테두리"""
|
|
# 상단
|
|
top_points = []
|
|
step_x = width / (steps * 2)
|
|
for i in range(steps * 2 + 1):
|
|
x = i * step_x
|
|
y = 0 if i % 2 == 0 else edge_size
|
|
top_points.append((int(x), int(y)))
|
|
draw.line(top_points, fill="black", width=2)
|
|
|
|
# 하단
|
|
bottom_points = []
|
|
for i in range(steps * 2 + 1):
|
|
x = i * step_x
|
|
y = height if i % 2 == 0 else height - edge_size
|
|
bottom_points.append((int(x), int(y)))
|
|
draw.line(bottom_points, fill="black", width=2)
|
|
|
|
# 좌측
|
|
left_points = []
|
|
step_y = height / (steps * 2)
|
|
for i in range(steps * 2 + 1):
|
|
y = i * step_y
|
|
x = 0 if i % 2 == 0 else edge_size
|
|
left_points.append((int(x), int(y)))
|
|
draw.line(left_points, fill="black", width=2)
|
|
|
|
# 우측
|
|
right_points = []
|
|
for i in range(steps * 2 + 1):
|
|
y = i * step_y
|
|
x = width if i % 2 == 0 else width - edge_size
|
|
right_points.append((int(x), int(y)))
|
|
draw.line(right_points, fill="black", width=2)
|
|
|
|
|
|
def print_drug_qr_label(drug_name, barcode, sale_price, drug_code=None, pharmacy_name='청춘약국'):
|
|
"""
|
|
약품 QR 라벨 인쇄 실행
|
|
|
|
Parameters:
|
|
drug_name (str): 약품명
|
|
barcode (str): 바코드
|
|
sale_price (float): 판매가격
|
|
drug_code (str, optional): 약품 코드
|
|
pharmacy_name (str, optional): 약국 이름
|
|
|
|
Returns:
|
|
dict: 성공/실패 결과
|
|
"""
|
|
try:
|
|
from brother_ql.raster import BrotherQLRaster
|
|
from brother_ql.conversion import convert
|
|
from brother_ql.backends.helpers import send
|
|
|
|
label_image = create_drug_qr_label(drug_name, barcode, sale_price, drug_code, pharmacy_name)
|
|
|
|
# 이미지를 메모리 스트림으로 변환
|
|
image_stream = io.BytesIO()
|
|
label_image.save(image_stream, format="PNG")
|
|
image_stream.seek(0)
|
|
|
|
# Brother QL 프린터로 전송
|
|
qlr = BrotherQLRaster(PRINTER_MODEL)
|
|
instructions = convert(
|
|
qlr=qlr,
|
|
images=[Image.open(image_stream)],
|
|
label=LABEL_TYPE,
|
|
rotate="0",
|
|
threshold=70.0,
|
|
dither=False,
|
|
compress=False,
|
|
lq=True,
|
|
red=False
|
|
)
|
|
send(instructions, printer_identifier=f"tcp://{PRINTER_IP}:9100")
|
|
|
|
logging.info(f"QR 라벨 인쇄 성공: {drug_name}, 바코드={barcode}")
|
|
return {"success": True, "message": f"{drug_name} QR 라벨 인쇄 완료"}
|
|
|
|
except ImportError as e:
|
|
logging.error(f"brother_ql 라이브러리 없음: {e}")
|
|
return {"success": False, "error": "brother_ql 라이브러리가 설치되지 않았습니다"}
|
|
except Exception as e:
|
|
logging.error(f"QR 라벨 인쇄 실패: {e}")
|
|
return {"success": False, "error": str(e)}
|
|
|
|
|
|
def preview_qr_label(drug_name, barcode, sale_price, drug_code=None, pharmacy_name='청춘약국'):
|
|
"""
|
|
QR 라벨 미리보기 (base64 이미지 반환)
|
|
"""
|
|
import base64
|
|
|
|
label_image = create_drug_qr_label(drug_name, barcode, sale_price, drug_code, pharmacy_name)
|
|
|
|
# PNG로 변환
|
|
image_stream = io.BytesIO()
|
|
# 1-bit 이미지를 RGB로 변환하여 더 깔끔하게
|
|
rgb_image = label_image.convert('RGB')
|
|
rgb_image.save(image_stream, format="PNG")
|
|
image_stream.seek(0)
|
|
|
|
base64_image = base64.b64encode(image_stream.read()).decode('utf-8')
|
|
return f"data:image/png;base64,{base64_image}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# 테스트
|
|
result = print_drug_qr_label(
|
|
drug_name="벤포파워Z",
|
|
barcode="8806418067510",
|
|
sale_price=3000,
|
|
pharmacy_name="청춘약국"
|
|
)
|
|
print(result)
|