Initial commit: 동물약 복약안내 PDF API

This commit is contained in:
청춘약국
2026-03-18 21:49:33 +09:00
commit 11f0a4c3c9
6 changed files with 847 additions and 0 deletions

18
.gitignore vendored Normal file
View 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
View File

@@ -0,0 +1,3 @@
from .renderer import AnimalMedRenderer
__all__ = ['AnimalMedRenderer']

205
animal_med/renderer.py Normal file
View 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
View 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": "동물약 복약안내 목업 데이터"
}
}

View 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
View 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()