feat(pmr): 라벨 디자인 개선 + 약품명 정규화

라벨 미리보기:
- 지그재그 테두리 (가위로 자른 느낌)
- 환자명 공백 + 폰트 확대 (44px)
- 복용량 박스 + 총량 표시
- 시그니처 박스 (청 춘 약 국)
- 조제일 표시

약품명 정규화:
- 밀리그램/밀리그람 → mg
- 마이크로그램 → μg
- 그램/그람 → g
- 밀리리터 → mL
- 언더스코어(_) 뒤 내용 제거
- 대괄호 내용 제거

프론트엔드:
- data-med-name 속성으로 순수 약품명 전달
- 번호/뱃지 제외된 이름 사용
This commit is contained in:
thug0bin 2026-03-11 22:13:08 +09:00
parent 849ce4c3c0
commit 7b71ea0179
2 changed files with 157 additions and 31 deletions

View File

@ -577,10 +577,82 @@ def preview_label():
return jsonify({'success': False, 'error': str(e)}), 500
def normalize_medication_name(med_name):
"""
약품명 정제 - 밀리그램 등을 mg로 변환, 불필요한 부분 제거
"""
import re
if not med_name:
return med_name
# 언더스코어 뒤 내용 제거 (예: 휴니즈레바미피드정_ → 휴니즈레바미피드정)
med_name = re.sub(r'_.*$', '', med_name)
# 대괄호 및 내용 제거
med_name = re.sub(r'\[.*?\]', '', med_name)
med_name = re.sub(r'\[.*$', '', med_name)
# 밀리그램 변환
med_name = re.sub(r'밀리그램|밀리그람|미리그램|미리그람', 'mg', med_name)
# 마이크로그램 변환
med_name = re.sub(r'마이크로그램|마이크로그람', 'μg', med_name)
# 그램 변환 (mg/μg 제외)
med_name = re.sub(r'(?<!m)(?<!μ)그램|그람', 'g', med_name)
# 밀리리터 변환
med_name = re.sub(r'밀리리터|밀리리타|미리리터|미리리타', 'mL', med_name)
# 공백 정리
med_name = re.sub(r'\s+', ' ', med_name).strip()
return med_name
def draw_scissor_border(draw, width, height, edge_size=5, 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 create_label_image(patient_name, med_name, add_info='', dosage=0, frequency=0, duration=0, unit=''):
"""
라벨 이미지 생성 (29mm 용지 기준)
라벨 이미지 생성 (29mm 용지 기준) - 레거시 디자인 적용
"""
# 약품명 정제 (밀리그램 → mg 등)
med_name = normalize_medication_name(med_name)
# 라벨 크기 (29mm 용지, 300dpi 기준)
label_width = 306
label_height = 380
@ -594,15 +666,38 @@ def create_label_image(patient_name, med_name, add_info='', dosage=0, frequency=
font_path = "C:/Windows/Fonts/malgun.ttf"
try:
name_font = ImageFont.truetype(font_path, 36)
drug_font = ImageFont.truetype(font_path, 24)
info_font = ImageFont.truetype(font_path, 22)
small_font = ImageFont.truetype(font_path, 18)
name_font = ImageFont.truetype(font_path, 44) # 환자명 폰트 크게
drug_font = ImageFont.truetype(font_path, 32) # 약품명
info_font = ImageFont.truetype(font_path, 30) # 복용 정보
small_font = ImageFont.truetype(font_path, 20) # 조제일
additional_font = ImageFont.truetype(font_path, 27) # 총량/효능
signature_font = ImageFont.truetype(font_path, 32) # 시그니처
except:
name_font = ImageFont.load_default()
drug_font = ImageFont.load_default()
info_font = ImageFont.load_default()
small_font = ImageFont.load_default()
additional_font = ImageFont.load_default()
signature_font = ImageFont.load_default()
# 동적 폰트 크기 조정 함수
def get_adaptive_font(text, max_width, initial_font_size, min_font_size=20):
"""텍스트가 max_width를 초과하지 않도록 폰트 크기를 동적으로 조정"""
current_size = initial_font_size
while current_size >= min_font_size:
try:
test_font = ImageFont.truetype(font_path, current_size)
except:
return ImageFont.load_default()
bbox = draw.textbbox((0, 0), text, font=test_font)
text_width = bbox[2] - bbox[0]
if text_width <= max_width:
return test_font
current_size -= 2
try:
return ImageFont.truetype(font_path, min_font_size)
except:
return ImageFont.load_default()
# 중앙 정렬 텍스트 함수
def draw_centered(text, y, font, fill="black"):
@ -650,33 +745,37 @@ def create_label_image(patient_name, med_name, add_info='', dosage=0, frequency=
for line in name_lines:
y = draw_centered(line, y, drug_font)
# 효능효과 (add_info)
# 효능효과 (add_info) - 동적 폰트 크기 적용
if add_info:
y = draw_centered(f"({add_info})", y, small_font, fill="gray")
efficacy_text = f"({add_info})"
adaptive_efficacy_font = get_adaptive_font(efficacy_text, label_width - 40, 30, 20)
y = draw_centered(efficacy_text, y, adaptive_efficacy_font, fill="black")
y += 5
# 총량 계산
# 총량 계산 및 표시
if dosage > 0 and frequency > 0 and duration > 0:
total = dosage * frequency * duration
total_str = str(int(total)) if total == int(total) else f"{total:.1f}"
total_text = f" {total_str}{unit} / {duration}일분"
y = draw_centered(total_text, y, info_font)
total_str = str(int(total)) if total == int(total) else f"{total:.2f}".rstrip('0').rstrip('.')
total_text = f"{total_str}{unit}/{duration}일분"
y = draw_centered(total_text, y, additional_font)
y += 5
# 용법 박스
# 용법 박스 (테두리 있는 박스)
box_margin = 20
box_height = 75
box_top = y
box_bottom = y + 70
box_bottom = y + box_height
box_width = label_width - 2 * box_margin
draw.rectangle(
[(box_margin, box_top), (label_width - box_margin, box_bottom)],
outline="black",
width=2
)
# 박스 내용
dosage_str = str(int(dosage)) if dosage == int(dosage) else f"{dosage:.2f}".rstrip('0').rstrip('.')
# 박스 내용 - 1회 복용량
dosage_str = str(int(dosage)) if dosage == int(dosage) else f"{dosage:.4f}".rstrip('0').rstrip('.')
dosage_text = f"{dosage_str}{unit}"
# 복용 시간
@ -689,24 +788,51 @@ def create_label_image(patient_name, med_name, add_info='', dosage=0, frequency=
else:
time_text = f"1일 {frequency}"
box_center_y = (box_top + box_bottom) // 2
draw_centered(dosage_text, box_center_y - 20, info_font)
draw_centered(time_text, box_center_y + 5, info_font)
# 박스 내 텍스트 중앙 배치 (수직 중앙 정렬)
line_spacing = 5
bbox1 = draw.textbbox((0, 0), dosage_text, font=info_font)
text1_height = bbox1[3] - bbox1[1]
bbox2 = draw.textbbox((0, 0), time_text, font=info_font)
text2_height = bbox2[3] - bbox2[1]
total_text_height = text1_height + line_spacing + text2_height
center_y = (box_top + box_bottom) // 2
start_y = center_y - (total_text_height // 2) - 5 # 약간 위로 조정
draw_centered(dosage_text, start_y, info_font)
draw_centered(time_text, start_y + text1_height + line_spacing, info_font)
y = box_bottom + 10
# 조제일
# 조제일 (시그니처 위쪽에 배치)
today = datetime.now().strftime('%Y-%m-%d')
y = draw_centered(f"조제일: {today}", y, small_font)
print_date_text = f"조제일 : {today}"
bbox = draw.textbbox((0, 0), print_date_text, font=small_font)
date_w, date_h = bbox[2] - bbox[0], bbox[3] - bbox[1]
print_date_y = label_height - date_h - 70 # 시그니처 위쪽
draw.text(((label_width - date_w) / 2, print_date_y), print_date_text, font=small_font, fill="black")
# 약국명 (하단)
pharmacy_y = label_height - 40
draw.rectangle(
[(50, pharmacy_y - 5), (label_width - 50, pharmacy_y + 25)],
outline="black",
width=1
)
draw_centered("청 춘 약 국", pharmacy_y, info_font)
# 시그니처 박스 (하단 - 약국명)
signature_text = "청 춘 약 국"
bbox = draw.textbbox((0, 0), signature_text, font=signature_font)
w_sig, h_sig = bbox[2] - bbox[0], bbox[3] - bbox[1]
# 시그니처 박스 패딩 및 위치 계산
padding_top = int(h_sig * 0.1)
padding_bottom = int(h_sig * 0.5)
padding_sides = int(h_sig * 0.2)
box_x = (label_width - w_sig) / 2 - padding_sides
box_y = label_height - h_sig - padding_top - padding_bottom - 10
box_x2 = box_x + w_sig + 2 * padding_sides
box_y2 = box_y + h_sig + padding_top + padding_bottom
# 시그니처 테두리 및 텍스트
draw.rectangle([(box_x, box_y), (box_x2, box_y2)], outline="black", width=1)
draw.text(((label_width - w_sig) / 2, box_y + padding_top), signature_text, font=signature_font, fill="black")
# 지그재그 테두리 (가위로 자른 느낌)
draw_scissor_border(draw, label_width, label_height, edge_size=10, steps=20)
return image

View File

@ -1562,7 +1562,7 @@
</thead>
<tbody>
${data.medications.map((m, i) => `
<tr data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}" ${m.is_substituted ? 'class="substituted-row"' : ''}>
<tr data-add-info="${escapeHtml(m.add_info || '')}" data-unit="${m.unit || '정'}" data-med-name="${escapeHtml(m.med_name || m.medication_code)}" ${m.is_substituted ? 'class="substituted-row"' : ''}>
<td><input type="checkbox" class="med-check" data-code="${m.medication_code}" ${m.is_auto_print ? 'checked' : ''}></td>
<td>
<div class="med-name">
@ -2600,8 +2600,8 @@
const tr = checkbox.closest('tr');
const cells = tr.querySelectorAll('td');
// 약품명: 두 번째 셀의 .med-name
const medName = tr.querySelector('.med-name')?.textContent?.trim() || '';
// 약품명: data-med-name 속성에서 (번호/뱃지 제외된 순수 약품명)
const medName = tr.dataset.medName || '';
const addInfo = tr.dataset.addInfo || '';
// 용량: 세 번째 셀 (index 2) - 제형 컬럼 제거됨
const dosageText = cells[2]?.textContent?.replace(/[^0-9.]/g, '') || '0';