Initial commit: 동물약 복약안내 PDF API
This commit is contained in:
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
dist/
|
||||
build/
|
||||
venv/
|
||||
.env
|
||||
|
||||
# Output
|
||||
output/
|
||||
*.pdf
|
||||
*.png
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
3
animal_med/__init__.py
Normal file
3
animal_med/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .renderer import AnimalMedRenderer
|
||||
|
||||
__all__ = ['AnimalMedRenderer']
|
||||
205
animal_med/renderer.py
Normal file
205
animal_med/renderer.py
Normal file
@@ -0,0 +1,205 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
동물약 복약안내 PDF 렌더러
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
|
||||
class AnimalMedRenderer:
|
||||
"""동물약 복약안내 PDF 렌더링 API"""
|
||||
|
||||
def __init__(self, data_path: str = None, template_path: str = None):
|
||||
base_dir = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
self.data_path = data_path or os.path.join(base_dir, 'data', 'mock_drugs.json')
|
||||
self.template_dir = template_path or os.path.join(base_dir, 'templates')
|
||||
|
||||
# Jinja2 템플릿 환경
|
||||
self.jinja_env = Environment(
|
||||
loader=FileSystemLoader(self.template_dir),
|
||||
autoescape=True
|
||||
)
|
||||
|
||||
# 약품 데이터 로드
|
||||
self._load_drug_data()
|
||||
|
||||
def _load_drug_data(self):
|
||||
"""약품 데이터 로드"""
|
||||
with open(self.data_path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
self.drugs = {drug['apc_code']: drug for drug in data['drugs']}
|
||||
self.metadata = data.get('metadata', {})
|
||||
|
||||
def get_drug(self, apc_code: str) -> Optional[Dict]:
|
||||
"""APC 코드로 약품 조회"""
|
||||
return self.drugs.get(apc_code)
|
||||
|
||||
def get_drugs(self, apc_codes: List[str]) -> List[Dict]:
|
||||
"""여러 APC 코드로 약품 조회"""
|
||||
result = []
|
||||
for code in apc_codes:
|
||||
drug = self.get_drug(code)
|
||||
if drug:
|
||||
result.append(drug)
|
||||
return result
|
||||
|
||||
def render_html(
|
||||
self,
|
||||
apc_codes: List[str],
|
||||
patient_name: str = "홍길동",
|
||||
pet_name: str = "뽀삐",
|
||||
pet_species: str = "개",
|
||||
pet_age: str = "3세",
|
||||
pharmacy_name: str = "청춘약국",
|
||||
pharmacy_tel: str = "033-481-7390",
|
||||
pharmacy_address: str = "강원 양구군 양구읍 양구새싹로 7-3"
|
||||
) -> str:
|
||||
"""
|
||||
APC 코드 배열을 받아 HTML 복약안내문 렌더링
|
||||
|
||||
Args:
|
||||
apc_codes: 약품 APC 코드 배열
|
||||
patient_name: 보호자명
|
||||
pet_name: 반려동물 이름
|
||||
pet_species: 동물 종류
|
||||
pet_age: 나이
|
||||
pharmacy_name: 약국명
|
||||
pharmacy_tel: 전화번호
|
||||
pharmacy_address: 주소
|
||||
|
||||
Returns:
|
||||
렌더링된 HTML 문자열
|
||||
"""
|
||||
drugs = self.get_drugs(apc_codes)
|
||||
|
||||
if not drugs:
|
||||
raise ValueError(f"유효한 약품을 찾을 수 없습니다: {apc_codes}")
|
||||
|
||||
template = self.jinja_env.get_template('medication_guide.html')
|
||||
|
||||
html = template.render(
|
||||
drugs=drugs,
|
||||
patient_name=patient_name,
|
||||
pet_name=pet_name,
|
||||
pet_species=pet_species,
|
||||
pet_age=pet_age,
|
||||
pharmacy_name=pharmacy_name,
|
||||
pharmacy_tel=pharmacy_tel,
|
||||
pharmacy_address=pharmacy_address,
|
||||
issue_date=datetime.now().strftime('%Y년 %m월 %d일')
|
||||
)
|
||||
|
||||
return html
|
||||
|
||||
def render_to_pdf(
|
||||
self,
|
||||
apc_codes: List[str],
|
||||
output_path: str = None,
|
||||
**kwargs
|
||||
) -> Dict:
|
||||
"""
|
||||
APC 코드 배열을 받아 PDF 복약안내문 생성
|
||||
|
||||
Args:
|
||||
apc_codes: 약품 APC 코드 배열
|
||||
output_path: 출력 PDF 경로
|
||||
**kwargs: render_html에 전달할 추가 인자
|
||||
|
||||
Returns:
|
||||
{
|
||||
'success': True,
|
||||
'pdf_path': '/path/to/file.pdf',
|
||||
'drug_count': 3,
|
||||
'drugs': [약품명 목록]
|
||||
}
|
||||
"""
|
||||
# HTML 렌더링
|
||||
html_content = self.render_html(apc_codes, **kwargs)
|
||||
|
||||
# 출력 경로
|
||||
if not output_path:
|
||||
import tempfile
|
||||
import time
|
||||
output_path = os.path.join(tempfile.gettempdir(), f'animal_med_{int(time.time())}.pdf')
|
||||
|
||||
# HTML → PDF
|
||||
async def _convert():
|
||||
from playwright.async_api import async_playwright
|
||||
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch(headless=True)
|
||||
|
||||
# A4 크기 viewport (210mm x 297mm @ 96dpi)
|
||||
page = await browser.new_page(viewport={'width': 794, 'height': 1123})
|
||||
|
||||
await page.set_content(html_content, wait_until='networkidle')
|
||||
|
||||
# 콘텐츠 크기 확인
|
||||
rect = await page.evaluate('''() => {
|
||||
const el = document.querySelector('.container');
|
||||
const rect = el.getBoundingClientRect();
|
||||
return { width: rect.width, height: rect.height };
|
||||
}''')
|
||||
|
||||
# A4 (595 x 842 pt)에 맞는 scale 계산
|
||||
content_width_pt = rect['width'] * 0.75
|
||||
content_height_pt = rect['height'] * 0.75
|
||||
|
||||
scale_x = 595 / content_width_pt if content_width_pt > 0 else 1
|
||||
scale_y = 842 / content_height_pt if content_height_pt > 0 else 1
|
||||
scale = min(scale_x, scale_y, 1.0) # 최대 1.0
|
||||
|
||||
# PDF 생성
|
||||
await page.pdf(
|
||||
path=output_path,
|
||||
format='A4',
|
||||
print_background=True,
|
||||
margin={'top': '0', 'right': '0', 'bottom': '0', 'left': '0'},
|
||||
scale=scale
|
||||
)
|
||||
|
||||
await browser.close()
|
||||
|
||||
try:
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
loop = None
|
||||
|
||||
if loop and loop.is_running():
|
||||
import concurrent.futures
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(asyncio.run, _convert())
|
||||
future.result()
|
||||
else:
|
||||
asyncio.run(_convert())
|
||||
|
||||
drugs = self.get_drugs(apc_codes)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'pdf_path': output_path,
|
||||
'drug_count': len(drugs),
|
||||
'drugs': [d['name'] for d in drugs]
|
||||
}
|
||||
except Exception as e:
|
||||
return {'success': False, 'error': str(e)}
|
||||
|
||||
def list_drugs(self) -> List[Dict]:
|
||||
"""모든 약품 목록 반환"""
|
||||
return [
|
||||
{
|
||||
'apc_code': d['apc_code'],
|
||||
'name': d['name'],
|
||||
'category': d['category'],
|
||||
'target_animal': d['target_animal']
|
||||
}
|
||||
for d in self.drugs.values()
|
||||
]
|
||||
209
data/mock_drugs.json
Normal file
209
data/mock_drugs.json
Normal file
@@ -0,0 +1,209 @@
|
||||
{
|
||||
"drugs": [
|
||||
{
|
||||
"apc_code": "0519012001",
|
||||
"name": "아목시실린 정",
|
||||
"english_name": "Amoxicillin Tab",
|
||||
"manufacturer": "한국동물약품",
|
||||
"category": "항생제",
|
||||
"target_animal": ["개", "고양이"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/amoxicillin.jpg",
|
||||
"ingredients": "아목시실린트리하이드레이트 250mg",
|
||||
"efficacy": "세균성 감염증 치료. 피부감염, 요로감염, 호흡기감염 등에 효과적입니다.",
|
||||
"dosage": "체중 1kg당 12.5~25mg을 1일 2회, 5~7일간 투여",
|
||||
"precautions": [
|
||||
"페니실린 계열 알러지가 있는 동물에게 투여 금지",
|
||||
"신장 기능 저하 동물은 용량 조절 필요",
|
||||
"임신 중인 동물에게는 수의사와 상담 후 투여"
|
||||
],
|
||||
"storage": "실온(1~30℃) 보관, 직사광선 피할 것",
|
||||
"shelf_life": "제조일로부터 24개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519023002",
|
||||
"name": "엔로플록사신 정",
|
||||
"english_name": "Enrofloxacin Tab",
|
||||
"manufacturer": "바이엘동물약품",
|
||||
"category": "항생제",
|
||||
"target_animal": ["개", "고양이"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/enrofloxacin.jpg",
|
||||
"ingredients": "엔로플록사신 50mg",
|
||||
"efficacy": "그람양성균 및 그람음성균에 의한 감염증 치료. 피부, 비뇨기, 호흡기 감염에 효과적.",
|
||||
"dosage": "체중 1kg당 5mg을 1일 1회, 7~14일간 투여",
|
||||
"precautions": [
|
||||
"⚠️ 고양이 망막 독성 주의! 권장용량 초과 금지",
|
||||
"성장기 동물에게는 연골 손상 가능성 있음",
|
||||
"간질 병력이 있는 동물 주의"
|
||||
],
|
||||
"storage": "실온 보관, 습기 피할 것",
|
||||
"shelf_life": "제조일로부터 36개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519034003",
|
||||
"name": "프레드니솔론 정",
|
||||
"english_name": "Prednisolone Tab",
|
||||
"manufacturer": "녹십자수의약품",
|
||||
"category": "스테로이드",
|
||||
"target_animal": ["개", "고양이"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/prednisolone.jpg",
|
||||
"ingredients": "프레드니솔론 5mg",
|
||||
"efficacy": "염증성 질환, 알러지성 질환, 자가면역 질환의 치료",
|
||||
"dosage": "항염증: 체중 1kg당 0.5~1mg, 1일 1~2회\n면역억제: 체중 1kg당 2~4mg, 1일 2회",
|
||||
"precautions": [
|
||||
"장기 투여 시 부신기능 저하 주의",
|
||||
"당뇨병, 쿠싱증후군 동물 금기",
|
||||
"갑자기 중단하지 말고 서서히 감량"
|
||||
],
|
||||
"storage": "실온 보관",
|
||||
"shelf_life": "제조일로부터 24개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519045004",
|
||||
"name": "세파렉신 캡슐",
|
||||
"english_name": "Cephalexin Cap",
|
||||
"manufacturer": "한국동물약품",
|
||||
"category": "항생제",
|
||||
"target_animal": ["개"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/cephalexin.jpg",
|
||||
"ingredients": "세파렉신모노하이드레이트 500mg",
|
||||
"efficacy": "피부 및 연조직 감염, 요로감염, 골관절 감염 치료",
|
||||
"dosage": "체중 1kg당 22~30mg을 1일 2회, 7~28일간 투여",
|
||||
"precautions": [
|
||||
"세팔로스포린 계열 과민증 동물 금기",
|
||||
"음식과 함께 투여 시 흡수 증가",
|
||||
"신장 기능 저하 시 용량 조절"
|
||||
],
|
||||
"storage": "실온 보관, 개봉 후 습기 주의",
|
||||
"shelf_life": "제조일로부터 24개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519056005",
|
||||
"name": "메트로니다졸 정",
|
||||
"english_name": "Metronidazole Tab",
|
||||
"manufacturer": "동방동물약품",
|
||||
"category": "항생제/항원충제",
|
||||
"target_animal": ["개", "고양이"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/metronidazole.jpg",
|
||||
"ingredients": "메트로니다졸 250mg",
|
||||
"efficacy": "지아디아증, 혐기성 세균 감염, 염증성 장질환 치료",
|
||||
"dosage": "체중 1kg당 10~25mg을 1일 2회, 5~7일간 투여",
|
||||
"precautions": [
|
||||
"임신 동물 금기 (기형 유발 가능)",
|
||||
"간 질환 동물 용량 감소 필요",
|
||||
"고용량 장기 투여 시 신경독성 주의"
|
||||
],
|
||||
"storage": "차광 보관, 실온",
|
||||
"shelf_life": "제조일로부터 24개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519067006",
|
||||
"name": "트라마돌 정",
|
||||
"english_name": "Tramadol Tab",
|
||||
"manufacturer": "녹십자수의약품",
|
||||
"category": "진통제",
|
||||
"target_animal": ["개"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/tramadol.jpg",
|
||||
"ingredients": "트라마돌염산염 50mg",
|
||||
"efficacy": "중등도 이상의 통증 관리. 수술 후 통증, 골관절염 통증 완화",
|
||||
"dosage": "체중 1kg당 2~5mg을 1일 2~3회 투여",
|
||||
"precautions": [
|
||||
"⚠️ 고양이에게는 사용 금지 (세로토닌 증후군 위험)",
|
||||
"간질 병력 동물 주의",
|
||||
"MAO 억제제와 병용 금기"
|
||||
],
|
||||
"storage": "실온 보관",
|
||||
"shelf_life": "제조일로부터 36개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519078007",
|
||||
"name": "오메프라졸 캡슐",
|
||||
"english_name": "Omeprazole Cap",
|
||||
"manufacturer": "한국동물약품",
|
||||
"category": "위장관약",
|
||||
"target_animal": ["개", "고양이"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/omeprazole.jpg",
|
||||
"ingredients": "오메프라졸 20mg",
|
||||
"efficacy": "위산 과다 분비 억제. 위궤양, 역류성 식도염, 스트레스성 위염 치료",
|
||||
"dosage": "체중 1kg당 0.5~1mg을 1일 1회, 공복에 투여",
|
||||
"precautions": [
|
||||
"장기 투여 시 비타민 B12 흡수 저하 가능",
|
||||
"캡슐을 씹거나 부수지 말 것",
|
||||
"식전 30분~1시간 전 투여 권장"
|
||||
],
|
||||
"storage": "실온 보관, 습기 피할 것",
|
||||
"shelf_life": "제조일로부터 24개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519089008",
|
||||
"name": "아포퀠 정",
|
||||
"english_name": "Apoquel Tab",
|
||||
"manufacturer": "조에티스",
|
||||
"category": "피부과약",
|
||||
"target_animal": ["개"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/apoquel.jpg",
|
||||
"ingredients": "오클라시티닙말레산염 16mg",
|
||||
"efficacy": "아토피성 피부염 및 알러지성 피부염에 의한 가려움증 치료",
|
||||
"dosage": "체중 1kg당 0.4~0.6mg을 1일 2회(14일간), 이후 1일 1회 유지",
|
||||
"precautions": [
|
||||
"12개월 미만 강아지 금기",
|
||||
"심각한 감염증이 있는 경우 주의",
|
||||
"악성 종양 병력 동물 주의"
|
||||
],
|
||||
"storage": "실온 보관",
|
||||
"shelf_life": "제조일로부터 36개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519090009",
|
||||
"name": "밀베마이신 정",
|
||||
"english_name": "Milbemycin Tab",
|
||||
"manufacturer": "노바티스동물약품",
|
||||
"category": "구충제",
|
||||
"target_animal": ["개"],
|
||||
"administration": "경구투여",
|
||||
"image_url": "/images/milbemycin.jpg",
|
||||
"ingredients": "밀베마이신옥심 12.5mg",
|
||||
"efficacy": "심장사상충 예방, 회충/구충/편충/조충 구제",
|
||||
"dosage": "월 1회, 체중에 따라 1정 또는 분할 투여",
|
||||
"precautions": [
|
||||
"콜리 품종은 MDR1 유전자 검사 후 투여",
|
||||
"심장사상충 감염견에게 투여 시 쇼크 위험",
|
||||
"음식과 함께 투여 권장"
|
||||
],
|
||||
"storage": "실온 보관, 직사광선 피할 것",
|
||||
"shelf_life": "제조일로부터 24개월"
|
||||
},
|
||||
{
|
||||
"apc_code": "0519001010",
|
||||
"name": "클로르헥시딘 샴푸",
|
||||
"english_name": "Chlorhexidine Shampoo",
|
||||
"manufacturer": "비르박동물약품",
|
||||
"category": "피부외용제",
|
||||
"target_animal": ["개", "고양이"],
|
||||
"administration": "외용",
|
||||
"image_url": "/images/chlorhexidine_shampoo.jpg",
|
||||
"ingredients": "클로르헥시딘글루콘산염 2%",
|
||||
"efficacy": "세균성/진균성 피부염, 농피증, 피부 감염 예방 및 치료",
|
||||
"dosage": "주 2~3회, 피모를 적신 후 충분히 거품을 내어 5~10분간 방치 후 헹굼",
|
||||
"precautions": [
|
||||
"눈, 귀, 점막 부위 접촉 피할 것",
|
||||
"상처가 깊은 경우 사용 금지",
|
||||
"사용 후 완전히 헹궈낼 것"
|
||||
],
|
||||
"storage": "실온 보관, 직사광선 피할 것",
|
||||
"shelf_life": "제조일로부터 24개월"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"version": "1.0.0",
|
||||
"created": "2026-03-18",
|
||||
"description": "동물약 복약안내 목업 데이터"
|
||||
}
|
||||
}
|
||||
346
templates/medication_guide.html
Normal file
346
templates/medication_guide.html
Normal file
@@ -0,0 +1,346 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>동물약 복약안내문</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700&display=swap');
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
html, body {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
font-family: 'Noto Sans KR', sans-serif;
|
||||
font-size: 10pt;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 210mm;
|
||||
height: 297mm;
|
||||
padding: 8mm;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 헤더 */
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
border-bottom: 3px solid #2E7D32;
|
||||
padding-bottom: 4mm;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.header-left { flex: 1; }
|
||||
|
||||
.patient-name {
|
||||
font-size: 18pt;
|
||||
font-weight: 700;
|
||||
color: #1B5E20;
|
||||
}
|
||||
|
||||
.patient-info {
|
||||
font-size: 9pt;
|
||||
color: #666;
|
||||
margin-top: 2mm;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
text-align: center;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.pharmacy-name {
|
||||
font-size: 16pt;
|
||||
font-weight: 700;
|
||||
color: #2E7D32;
|
||||
}
|
||||
|
||||
.pharmacy-slogan {
|
||||
font-size: 9pt;
|
||||
color: #4CAF50;
|
||||
margin-top: 1mm;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
text-align: right;
|
||||
font-size: 8pt;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 건강정보 */
|
||||
.health-info {
|
||||
background: linear-gradient(135deg, #E8F5E9, #C8E6C9);
|
||||
border-radius: 2mm;
|
||||
padding: 3mm 4mm;
|
||||
margin-bottom: 4mm;
|
||||
}
|
||||
|
||||
.health-title {
|
||||
font-size: 11pt;
|
||||
font-weight: 700;
|
||||
color: #1B5E20;
|
||||
margin-bottom: 1mm;
|
||||
}
|
||||
|
||||
.health-content {
|
||||
font-size: 8pt;
|
||||
color: #33691E;
|
||||
}
|
||||
|
||||
/* 약품 카드 */
|
||||
.drug-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 3mm;
|
||||
}
|
||||
|
||||
.drug-card {
|
||||
width: calc(50% - 1.5mm);
|
||||
background: #fff;
|
||||
border: 1px solid #C8E6C9;
|
||||
border-radius: 2mm;
|
||||
overflow: hidden;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.drug-card.full-width { width: 100%; }
|
||||
|
||||
.drug-header {
|
||||
display: flex;
|
||||
background: #E8F5E9;
|
||||
padding: 2mm;
|
||||
gap: 2mm;
|
||||
}
|
||||
|
||||
.drug-image {
|
||||
width: 15mm;
|
||||
height: 15mm;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 1mm;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20pt;
|
||||
}
|
||||
|
||||
.drug-title { flex: 1; }
|
||||
|
||||
.drug-name {
|
||||
font-size: 10pt;
|
||||
font-weight: 700;
|
||||
color: #1B5E20;
|
||||
}
|
||||
|
||||
.drug-category {
|
||||
display: inline-block;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
font-size: 7pt;
|
||||
padding: 0.5mm 2mm;
|
||||
border-radius: 1mm;
|
||||
margin-top: 1mm;
|
||||
}
|
||||
|
||||
.target-animals {
|
||||
display: flex;
|
||||
gap: 1mm;
|
||||
margin-top: 1mm;
|
||||
}
|
||||
|
||||
.animal-badge {
|
||||
background: #81C784;
|
||||
color: white;
|
||||
font-size: 6pt;
|
||||
padding: 0.3mm 1.5mm;
|
||||
border-radius: 0.5mm;
|
||||
}
|
||||
|
||||
.drug-english {
|
||||
font-size: 6pt;
|
||||
color: #888;
|
||||
margin-top: 0.5mm;
|
||||
}
|
||||
|
||||
.drug-body {
|
||||
padding: 2mm;
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
.drug-section { margin-bottom: 1.5mm; }
|
||||
|
||||
.drug-section-title {
|
||||
font-weight: 600;
|
||||
color: #2E7D32;
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
.drug-section-content {
|
||||
color: #555;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
/* 용법용량 */
|
||||
.dosage-highlight {
|
||||
background: #E3F2FD;
|
||||
border: 1px solid #64B5F6;
|
||||
border-radius: 1.5mm;
|
||||
padding: 1.5mm 2mm;
|
||||
margin-top: 1.5mm;
|
||||
}
|
||||
|
||||
.dosage-title {
|
||||
font-weight: 700;
|
||||
color: #1565C0;
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
.dosage-content {
|
||||
color: #0D47A1;
|
||||
font-size: 7pt;
|
||||
margin-top: 0.5mm;
|
||||
}
|
||||
|
||||
/* 주의사항 */
|
||||
.caution-box {
|
||||
background: #FFF3E0;
|
||||
border: 1px solid #FFB74D;
|
||||
border-radius: 1.5mm;
|
||||
padding: 1.5mm 2mm;
|
||||
margin-top: 1.5mm;
|
||||
}
|
||||
|
||||
.caution-title {
|
||||
font-weight: 700;
|
||||
color: #E65100;
|
||||
font-size: 7pt;
|
||||
}
|
||||
|
||||
.caution-list {
|
||||
list-style: none;
|
||||
margin-top: 0.5mm;
|
||||
}
|
||||
|
||||
.caution-list li {
|
||||
font-size: 6.5pt;
|
||||
color: #BF360C;
|
||||
margin-bottom: 0.3mm;
|
||||
padding-left: 2mm;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.caution-list li::before {
|
||||
content: "•";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
/* 푸터 */
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 5mm;
|
||||
left: 8mm;
|
||||
right: 8mm;
|
||||
text-align: center;
|
||||
font-size: 7pt;
|
||||
color: #999;
|
||||
border-top: 1px solid #E0E0E0;
|
||||
padding-top: 2mm;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
font-weight: 700;
|
||||
color: #2E7D32;
|
||||
font-size: 9pt;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<!-- 헤더 -->
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<div class="patient-name">{{ patient_name }} 보호자님</div>
|
||||
<div class="patient-info">
|
||||
환자: {{ pet_name }} ({{ pet_species }}, {{ pet_age }})<br>
|
||||
조제일: {{ issue_date }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<div class="pharmacy-name">🐾 {{ pharmacy_name }}</div>
|
||||
<div class="pharmacy-slogan">반려동물의 건강한 삶을 위한 복약안내</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
{{ pharmacy_tel }}<br>
|
||||
{{ pharmacy_address }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 건강정보 -->
|
||||
<div class="health-info">
|
||||
<div class="health-title">💊 약사의 복약 안내</div>
|
||||
<div class="health-content">
|
||||
처방된 약품을 정확한 용법에 따라 투여해 주세요.
|
||||
투약 중 이상반응이 나타나면 즉시 투약을 중단하고 수의사 또는 약사에게 상담하세요.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 약품 카드들 -->
|
||||
<div class="drug-cards">
|
||||
{% for drug in drugs %}
|
||||
<div class="drug-card">
|
||||
<div class="drug-header">
|
||||
<div class="drug-image">💊</div>
|
||||
<div class="drug-title">
|
||||
<div class="drug-name">{{ drug.name }}</div>
|
||||
<span class="drug-category">{{ drug.category }}</span>
|
||||
<div class="target-animals">
|
||||
{% for animal in drug.target_animal %}
|
||||
<span class="animal-badge">{{ animal }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="drug-english">{{ drug.english_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="drug-body">
|
||||
<div class="drug-section">
|
||||
<div class="drug-section-title">성분</div>
|
||||
<div class="drug-section-content">{{ drug.ingredients }}</div>
|
||||
</div>
|
||||
<div class="drug-section">
|
||||
<div class="drug-section-title">효능효과</div>
|
||||
<div class="drug-section-content">{{ drug.efficacy }}</div>
|
||||
</div>
|
||||
|
||||
<div class="dosage-highlight">
|
||||
<div class="dosage-title">📋 용법용량</div>
|
||||
<div class="dosage-content">{{ drug.dosage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="caution-box">
|
||||
<div class="caution-title">⚠️ 주의사항</div>
|
||||
<ul class="caution-list">
|
||||
{% for precaution in drug.precautions %}
|
||||
<li>{{ precaution }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 푸터 -->
|
||||
<div class="footer">
|
||||
<div class="footer-logo">🐾 동물약 복약안내 시스템</div>
|
||||
※ 본 복약안내문은 참고용이며, 정확한 투약은 반드시 수의사의 지시에 따라 주세요.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
66
test_render.py
Normal file
66
test_render.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
동물약 복약안내 PDF 테스트
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from animal_med import AnimalMedRenderer
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("동물약 복약안내 PDF 테스트")
|
||||
print("=" * 60)
|
||||
|
||||
renderer = AnimalMedRenderer()
|
||||
|
||||
# 약품 목록 확인
|
||||
print("\n[1] 등록된 약품 목록:")
|
||||
for drug in renderer.list_drugs():
|
||||
print(f" {drug['apc_code']} - {drug['name']} ({drug['category']})")
|
||||
|
||||
# PDF 렌더링 테스트 (3개 약품)
|
||||
test_codes = ["0519012001", "0519023002", "0519034003"]
|
||||
|
||||
print(f"\n[2] PDF 렌더링 테스트 ({len(test_codes)}개 약품)")
|
||||
print(f" APC 코드: {test_codes}")
|
||||
|
||||
output_dir = os.path.join(os.path.dirname(__file__), 'output')
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
pdf_path = os.path.join(output_dir, 'animal_medication_guide.pdf')
|
||||
|
||||
result = renderer.render_to_pdf(
|
||||
apc_codes=test_codes,
|
||||
output_path=pdf_path,
|
||||
patient_name="김철수",
|
||||
pet_name="뽀삐",
|
||||
pet_species="포메라니안",
|
||||
pet_age="3세"
|
||||
)
|
||||
|
||||
if result['success']:
|
||||
print(f" ✅ 성공!")
|
||||
print(f" 📄 PDF: {result['pdf_path']}")
|
||||
print(f" 약품: {', '.join(result['drugs'])}")
|
||||
|
||||
# 파일 크기
|
||||
size = os.path.getsize(pdf_path)
|
||||
print(f" 크기: {size / 1024:.1f} KB")
|
||||
|
||||
# PDF 페이지 수
|
||||
import fitz
|
||||
doc = fitz.open(pdf_path)
|
||||
print(f" 페이지 수: {len(doc)}")
|
||||
doc.close()
|
||||
else:
|
||||
print(f" ❌ 실패: {result.get('error')}")
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user