From 11f0a4c3c91adc08150f7279c99ed4d86824e0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=AD=EC=B6=98=EC=95=BD=EA=B5=AD?= Date: Wed, 18 Mar 2026 21:49:33 +0900 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20=EB=8F=99=EB=AC=BC=EC=95=BD?= =?UTF-8?q?=20=EB=B3=B5=EC=95=BD=EC=95=88=EB=82=B4=20PDF=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 18 ++ animal_med/__init__.py | 3 + animal_med/renderer.py | 205 +++++++++++++++++++ data/mock_drugs.json | 209 +++++++++++++++++++ templates/medication_guide.html | 346 ++++++++++++++++++++++++++++++++ test_render.py | 66 ++++++ 6 files changed, 847 insertions(+) create mode 100644 .gitignore create mode 100644 animal_med/__init__.py create mode 100644 animal_med/renderer.py create mode 100644 data/mock_drugs.json create mode 100644 templates/medication_guide.html create mode 100644 test_render.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe49e6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.eggs/ +dist/ +build/ +venv/ +.env + +# Output +output/ +*.pdf +*.png + +# IDE +.vscode/ +.idea/ diff --git a/animal_med/__init__.py b/animal_med/__init__.py new file mode 100644 index 0000000..7734809 --- /dev/null +++ b/animal_med/__init__.py @@ -0,0 +1,3 @@ +from .renderer import AnimalMedRenderer + +__all__ = ['AnimalMedRenderer'] diff --git a/animal_med/renderer.py b/animal_med/renderer.py new file mode 100644 index 0000000..32b42f9 --- /dev/null +++ b/animal_med/renderer.py @@ -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() + ] diff --git a/data/mock_drugs.json b/data/mock_drugs.json new file mode 100644 index 0000000..0f6e504 --- /dev/null +++ b/data/mock_drugs.json @@ -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": "동물약 복약안내 목업 데이터" + } +} diff --git a/templates/medication_guide.html b/templates/medication_guide.html new file mode 100644 index 0000000..ec57c5a --- /dev/null +++ b/templates/medication_guide.html @@ -0,0 +1,346 @@ + + + + + + 동물약 복약안내문 + + + +
+ +
+
+
{{ patient_name }} 보호자님
+
+ 환자: {{ pet_name }} ({{ pet_species }}, {{ pet_age }})
+ 조제일: {{ issue_date }} +
+
+
+
🐾 {{ pharmacy_name }}
+
반려동물의 건강한 삶을 위한 복약안내
+
+
+ {{ pharmacy_tel }}
+ {{ pharmacy_address }} +
+
+ + +
+
💊 약사의 복약 안내
+
+ 처방된 약품을 정확한 용법에 따라 투여해 주세요. + 투약 중 이상반응이 나타나면 즉시 투약을 중단하고 수의사 또는 약사에게 상담하세요. +
+
+ + +
+ {% for drug in drugs %} +
+
+
💊
+
+
{{ drug.name }}
+ {{ drug.category }} +
+ {% for animal in drug.target_animal %} + {{ animal }} + {% endfor %} +
+
{{ drug.english_name }}
+
+
+
+
+
성분
+
{{ drug.ingredients }}
+
+
+
효능효과
+
{{ drug.efficacy }}
+
+ +
+
📋 용법용량
+
{{ drug.dosage }}
+
+ +
+
⚠️ 주의사항
+
    + {% for precaution in drug.precautions %} +
  • {{ precaution }}
  • + {% endfor %} +
+
+
+
+ {% endfor %} +
+ + + +
+ + diff --git a/test_render.py b/test_render.py new file mode 100644 index 0000000..640a24d --- /dev/null +++ b/test_render.py @@ -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()