diff --git a/data/drugs_from_md.json b/data/drugs_from_md.json new file mode 100644 index 0000000..ba4474f --- /dev/null +++ b/data/drugs_from_md.json @@ -0,0 +1,490 @@ +[ + { + "apc_code": "APC-ACTIBET-AMOXICILLIN-", + "name": "액티벳정", + "english_name": "Actibet Tablet", + "manufacturer": "홍익메디케어", + "category": "Aminopenicillin + β-Lactamase Inhibitor", + "target_animal": [ + "개", + "고양이" + ], + "administration": "경구 (PO)", + "ingredients": "amoxicillin: 40mg, potassium_clavulanate: 10mg", + "efficacy": "", + "dosage": "dog: 12.5 mg/kg (combined) PO q12h = 체중 4kg당 1정 BID; cat: 12.5 mg/kg (combined) PO q12h", + "precautions": [ + "페니실린 계열 과민반응 이력", + "구토", + "설사", + "식욕부진 (위장관). 드물게: 알레르기 반응 (두드러기", + "안면부종", + "아나필락시스)" + ], + "storage": "25℃ 이하, 건조, 차광 보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "actibet_amoxicillin_clavulanate.md" + }, + { + "apc_code": "APC-ANTELMIN-DEWORMING-I", + "name": "대성 안텔민 뽀삐 정", + "english_name": "Mebendazole + Praziquantel", + "manufacturer": "미상", + "category": "복합 내부구충제 (벤즈이미다졸계 + 프라지노이소퀴놀린계)", + "target_animal": [ + "개", + "고양이" + ], + "administration": "경구", + "ingredients": "Mebendazole + Praziquantel", + "efficacy": "", + "dosage": "poppy: 체중 5kg 이하: 1정, 1일 1회, 1~2일간; king_5_9kg: 1정, 1일 1회, 1~2일간; king_10_19kg: 2정, 1일 1회, 1~2일간; king_20_30kg: 3정, 1일 1회, 1~2일간; king_30kg_plus: 4정, 1일 1회, 1~2일간", + "precautions": [ + "생후 1주 미만", + "임신 초기", + "쇠약 동물" + ], + "storage": "차광된 실온(1~30℃)", + "shelf_life": "제조일로부터 24개월", + "source_file": "antelmin_deworming_interval.md" + }, + { + "apc_code": "APC-ASIENRO-ENROFLOXACIN", + "name": "아시엔로50", + "english_name": "Asienro 50", + "manufacturer": "미상", + "category": "Fluoroquinolone (3rd generation Quinolone)", + "target_animal": [ + "개", + "고양이 (⚠️ 망막독성 주의)" + ], + "administration": "경구 (PO)", + "ingredients": "enrofloxacin: 50mg", + "efficacy": "", + "dosage": "dog: 5 mg/kg PO SID (= 체중 10kg당 1정, 1일 1회); cat: 5 mg/kg PO SID ⚠️ 절대 초과 금지", + "precautions": [ + "소형견 12개월 미만 / 대형견 18개월 미만 (연골 손상)", + "고양이: 5mg/kg/day 초과 금지 (망막독성)", + "간/신장 질환 동물 (약물 축적 위험)", + "FQ 과민반응 이력" + ], + "storage": "실온 보관, 차광", + "shelf_life": "제조일로부터 24개월", + "source_file": "asienro_enrofloxacin.md" + }, + { + "apc_code": "APC-ASIKAFF-CARPROFEN-PA", + "name": "아시카프 츄어블정", + "english_name": "Carprofen", + "manufacturer": "미상", + "category": "프로피온산계 NSAIDs (COX-2 우선 억제)", + "target_animal": [ + "개" + ], + "administration": "경구 (oral) — 츄어블정", + "ingredients": "Rimadyl® (Zoetis), Carprodyl®", + "efficacy": "개의 통증·염증 완화, 관절염", + "dosage": "once_daily: 4.4mg/kg 1일 1회 (식사와 함께); twice_daily: 2.2mg/kg 1일 2회 (식사와 함께); weight_based: 체중 5.5kg마다 0.5정(1일 1회) 또는 0.25정씩 2회", + "precautions": [ + "고양이 (절대 금기)", + "임신", + "수유 암캐", + "코르티코스테로이드 병용", + "다른 NSAIDs 병용", + "Carprofen 과민반응", + "GI 미란(만성 투여 83.3%", + "PMID 33534961)", + "간독성(특이체질성", + "예측 불가)", + "신독성(탈수", + "저혈압 시)", + "일과성 구토", + "식욕부진" + ], + "storage": "실온보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "asikaff_carprofen_pain_nsaid.md" + }, + { + "apc_code": "APC-FRONILSPOT-ECTOPARAS", + "name": "프로닐스팟 (Fronil Spot)", + "english_name": "Fronil Spot", + "manufacturer": "미상", + "category": "Phenylpyrazole (페닐피라졸계 살충/살비제)", + "target_animal": [ + "개" + ], + "administration": "경피 도포 (Spot-on, topical)", + "ingredients": "Fipronil (피프로닐)", + "efficacy": "벼룩(Ctenocephalides spp.) 및 진드기(Rhipicephalus sanguineus 등) 구제", + "dosage": "small_2_10kg: 0.67 mL (67 mg fipronil); medium_10_20kg: 1.34 mL (134 mg fipronil); large_20_40kg: 2.68 mL (268 mg fipronil)", + "precautions": [ + "8주(2개월) 미만", + "2kg 미만", + "질병/회복기 동물", + "피부 상처 부위", + "도포 부위 일시적 가려움/발적", + "경구 섭취 시 구토/침흘림", + "드물게 신경 증상" + ], + "storage": "직사광선 피하여 서늘한 곳 보관, 어린이 손에 닿지 않는 곳", + "shelf_life": "제조일로부터 24개월", + "source_file": "fronilspot_ectoparasite_control.md" + }, + { + "apc_code": "APC-GAESIDIN-GEL-PYODERM", + "name": "복합 개시딘 겔", + "english_name": "GaeSidin", + "manufacturer": "(주)이엘티사이언스 — 국내 제조", + "category": "항생제(스테로이드 구조 항생제) + 코르티코스테로이드 복합 외용 겔", + "target_animal": [ + "개" + ], + "administration": "피부 국소 도포 (외용)", + "ingredients": "", + "efficacy": "표면성 세균성 농피증 국소 치료", + "dosage": "", + "precautions": [ + "진균성(곰팡이) 농피증", + "임신·수유 중인 동물", + "성분 과민반응 동물", + "고양이 (개 전용)" + ], + "storage": "실온보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "gaesidin_gel_pyoderma_fusidic_acid.md" + }, + { + "apc_code": "APC-HEARTSAVER-CHEWABLE-", + "name": "중앙바이오텍 하트세이버 츄어블", + "english_name": "Ivermectin + Pyrantel Pamoate", + "manufacturer": "미상", + "category": "심장사상충 예방제 + 내부구충제 (Macrocyclic lactone + Tetrahydropyrimidine)", + "target_animal": [ + "개" + ], + "administration": "경구 (츄어블 정제)", + "ingredients": "Ivermectin (이버멕틴) + Pyrantel Pamoate (피란텔 파모에이트)", + "efficacy": "심장사상충(Dirofilaria immitis) 감염 예방, 회충(Toxocara canis, T. leonina) 및 구충(Ancylostoma caninum, A. braziliense, Uncinaria stenocephala) 구제", + "dosage": "mini: 체중 5.6kg 이하: 1정 (Ivermectin 68mcg + Pyrantel 57mg); small: 체중 5.7~11kg: 1정 (Ivermectin 136mcg + Pyrantel 114mg); medium: 체중 12~22kg: 1정 (Ivermectin 272mcg + Pyrantel 227mg); large: 체중 23~45kg: 1정 (Ivermectin 544mcg + Pyrantel 454mg); dose_per_kg: Ivermectin 최소 6 mcg/kg, Pyrantel 최소 5 mg/kg", + "precautions": [ + "심장사상충 양성 판정견 — 반드시 검사 후 투약 시작. 감염 상태에서 예방약 투여 시 마이크로필라리아 대량 사멸에 의한 쇼크 반응 가능. MDR1(ABCB1) 변이 동형접합 품종(콜리 등)에서 고용량 사용 금지 (예방 용량 6mcg/kg은 안전).", + "드물게 구토", + "식욕 감소", + "무기력. MDR1 변이 동형접합 개체에서 고용량 시: 산동", + "운동실조", + "진전", + "혼수 (예방 용량에서는 보고 없음)" + ], + "storage": "실온 보관, 직사광선 및 습기 회피", + "shelf_life": "제조일로부터 24개월", + "source_file": "heartsaver_chewable_dosing.md" + }, + { + "apc_code": "APC-IMPACT-SPOTON-MOXIDE", + "name": "제이에스 임팩트액 독", + "english_name": "Impact solution for dogs", + "manufacturer": "제이에스코리아", + "category": "복합 외부·내부 구충제 (스팟온 / Spot-on)", + "target_animal": [ + "개" + ], + "administration": "경피 도포 (Spot-on)", + "ingredients": "", + "efficacy": "", + "dosage": "", + "precautions": [ + "7주령 미만", + "MDR1 결핍 견종 신중 투여", + "눈", + "입 접촉 금지", + "일시적 가려움", + "피모 거침", + "부종", + "구토", + "핥을 경우 신경증상(보행장애", + "떨림", + "동공산대) 가능" + ], + "storage": "직사광선 회피, 어린이 접근 금지", + "shelf_life": "제조일로부터 24개월", + "source_file": "impact_spoton_moxidectin_imidacloprid.md" + }, + { + "apc_code": "APC-MELOXICASH-MELOXICAM", + "name": "멜록시캐시 CH 1mg 츄어블정", + "english_name": "Meloxicam", + "manufacturer": "미상", + "category": "옥시캄계 NSAIDs (COX-2 강선택 억제)", + "target_animal": [ + "개" + ], + "administration": "경구 (oral) — 츄어블정 1mg", + "ingredients": "Metacam® (Boehringer Ingelheim), Meloxidyl®, Loxicom®", + "efficacy": "개의 급성·만성 근골격계 질환 통증경감 및 염증 치료", + "dosage": "", + "precautions": [ + "탈수", + "저혈압", + "위장관", + "간", + "심장", + "신장 기저 이상", + "다른 NSAIDs 병용", + "코르티코스테로이드 병용", + "이뇨제", + "항응고제", + "아미노글리코사이드 병용", + "임신", + "수유", + "6주령 이하", + "고양이(이 제품)", + "GI 미란", + "궤양(NSAID 병용 시 천공 보고)", + "신독성(탈수", + "저혈압 시)", + "간독성(카프로펜보다 드뭄)" + ], + "storage": "실온보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "meloxicash_meloxicam_pain_nsaid.md" + }, + { + "apc_code": "APC-NEXGARD-SPECTRA-ALLI", + "name": "넥스가드 스펙트라 (NexGard Spectra)", + "english_name": "NexGard Spectra", + "manufacturer": "Boehringer Ingelheim (베링거인겔하임)", + "category": "Isoxazoline + Macrocyclic Lactone", + "target_animal": [ + "개" + ], + "administration": "경구 투여 (Oral chewable)", + "ingredients": "Afoxolaner (아폭솔라너) + Milbemycin Oxime (밀베마이신 옥심)", + "efficacy": "", + "dosage": "xs_2_3.5kg: 9.4 mg afoxolaner / 1.9 mg milbemycin oxime; s_3.6_7.5kg: 18.8 mg / 3.8 mg; m_7.6_15kg: 37.5 mg / 7.5 mg; l_15.1_30kg: 75 mg / 15 mg; xl_30.1_60kg: 150 mg / 30 mg", + "precautions": [ + "8주 미만", + "2kg 미만", + "경련/간질 이력(수의사 상담 필수)", + "심장사상충 미검사 상태", + "드물게 구토", + "설사", + "무기력", + "식욕부진. FDA 경고: 이소자졸린 계열 드물게 근육떨림/경련 가능" + ], + "storage": "직사광선 피하여 서늘한 곳 보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "nexgard_spectra_allinone.md" + }, + { + "apc_code": "APC-10", + "name": "오리더밀", + "english_name": "ORIDERMYL", + "manufacturer": "미상", + "category": "항생제+항진균제+스테로이드+살충제 복합 국소제", + "target_animal": [ + "개", + "고양이" + ], + "administration": "귓속 국소 도포", + "ingredients": "", + "efficacy": "세균성·곰팡이성 귓병 및 귀 주위 피부염, 귀진드기증", + "dosage": "", + "precautions": [ + "고막 파열된 동물", + "1.5kg 미만 고양이", + "임신·수유 중인 동물", + "경미한 자극", + "고양이에서 ALT/AST 일시적 상승 가능" + ], + "storage": "일반 보관 (냉장 불필요), 개봉 후 28일 이내 사용", + "shelf_life": "제조일로부터 24개월", + "source_file": "oridermyl_otitis_earmite.md" + }, + { + "apc_code": "APC-ORIMODEM-OTITIS-EXTE", + "name": "오리모덤 이어드롭스", + "english_name": "Gentamicin sulfate + Betamethasone valerate + Clotrimazole", + "manufacturer": "미상", + "category": "항균·항진균·스테로이드 복합 외용 점이제", + "target_animal": [ + "개" + ], + "administration": "외이도 점이 (otic)", + "ingredients": "Otomax® (Merck Animal Health / MSD)", + "efficacy": "개의 급성 및 만성 외이도염", + "dosage": "under_15kg: 4방울/회; over_15kg: 8방울/회", + "precautions": [ + "아미노글리코사이드 과민반응 병력", + "고막 천공 (주의)", + "임신", + "수유 중 암캐", + "신생축", + "고양이", + "국소: 일시적 작열감", + "스테로이드 장기 사용 시 이도 피부 위축. 전신: 드물게 스테로이드 흡수 (고막 손상 시)" + ], + "storage": "실온 보관, 직사광선 피함", + "shelf_life": "제조일로부터 24개월", + "source_file": "orimodem_otitis_externa.md" + }, + { + "apc_code": "APC-OTIMAXI-EAR-CLEANER", + "name": "오티맥시", + "english_name": "Otimaxi", + "manufacturer": "에스비바이오팜(주) — 국내 제조", + "category": "귀 세정·위생제 (치료제 아님)", + "target_animal": [ + "개", + "고양이" + ], + "administration": "외이도 내 점적", + "ingredients": "", + "efficacy": "", + "dosage": "", + "precautions": [ + "3개월 이하 강아지", + "고막 파열 의심 동물 (이독성 위험)", + "성분 과민반응 동물" + ], + "storage": "실온보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "otimaxi_ear_cleaner.md" + }, + { + "apc_code": "APC-SELIGHT-SELAMECTIN-S", + "name": "셀라이트 (Selight)", + "english_name": "Selight", + "manufacturer": "미상", + "category": "Avermectin (아버멕틴계 Macrocyclic Lactone, 내·외부 구충제)", + "target_animal": [ + "개", + "고양이" + ], + "administration": "경피 도포 (Spot-on, topical)", + "ingredients": "Selamectin (셀라멕틴)", + "efficacy": "", + "dosage": "SS_under_2.5kg: 0.25 mL (60 mg/mL); S_2.5_5kg: 0.5 mL (60 mg/mL); M_5_10kg: 0.5 mL (120 mg/mL); L_10_20kg: 1.0 mL (120 mg/mL); XL_20_40kg: 2.0 mL (120 mg/mL)", + "precautions": [ + "6주 미만", + "질병/회복기 동물", + "피부 상처 부위", + "도포 부위 일시적 발적/탈모", + "드물게 구토/설사/근육 떨림/무기력. 경구 섭취 시 일시적 침흘림/구토(고양이)" + ], + "storage": "직사광선 피하여 서늘한 곳 보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "selight_selamectin_spoton.md" + }, + { + "apc_code": "APC-SERENIA-MAROPITANT-A", + "name": "세레니아정 (Serenia Tablets)", + "english_name": "Serenia® Tablets", + "manufacturer": "미상", + "category": "선택적 NK1 수용체 길항제 (Selective Neurokinin-1 Receptor Antagonist, 항구토제)", + "target_animal": [ + "개", + "고양이" + ], + "administration": "경구 (Oral, PO) 또는 피하주사 (SC)", + "ingredients": "Maropitant Citrate (마로피턴트 시트르산염)", + "efficacy": "", + "dosage": "", + "precautions": [ + "주사 부위 통증(metacresol 방부제)", + "드물게 과다침분비", + "무기력", + "식욕감소. 경구 정제는 부작용 드뭄." + ], + "storage": "실온, 직사광선 피해 보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "serenia_maropitant_antiemetic.md" + }, + { + "apc_code": "APC-SKINCASOL-SKIN-CARE-", + "name": "스킨카솔 스프레이", + "english_name": "Centella asiatica Leaf Extract + Melaleuca alternifolia Oil + Grapefruit Seed Extract + Avena sativa Kernel Extract + Aloe barbadensis Leaf Extract + Essential Oils Complex", + "manufacturer": "(주)이엘티사이언스", + "category": "동물용 의약외품 — 복합 천연성분 피부케어", + "target_animal": [ + "개", + "고양이" + ], + "administration": "외용 (피부 분무)", + "ingredients": "", + "efficacy": "반려동물의 스킨케어", + "dosage": "spray: 1일 2~3회 피부에 뿌려준다", + "precautions": [ + "눈", + "코", + "입에 직접 분무 금지", + "부작용(붉은 반점", + "부어오름", + "가려움", + "자극) 시 즉시 중단", + "사람 사용 금지", + "고양이 사용 전 수의사 상담 권장", + "드물게 접촉성 피부염", + "자극 반응 (에센셜 오일 과민)" + ], + "storage": "직사광선 회피, 상온 보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "skincasol_skin_care_centella.md" + }, + { + "apc_code": "APC-TERBIDERM-SPRAY-CURE", + "name": "터비덤 스프레이 큐어", + "english_name": "Terbiderm Spray Cure", + "manufacturer": "(주)케어사이드 — 국내 제조", + "category": "알릴아민계 항진균제 + 비구아나이드계 소독·항균제 복합 외용제", + "target_animal": [ + "개" + ], + "administration": "피부 국소 분무", + "ingredients": "", + "efficacy": "", + "dosage": "", + "precautions": [ + "눈·코·입·점막 적용 금지", + "상처·긁힌 피부 금지", + "고양이 (미허가)", + "성분 과민반응 동물", + "알레르기 반응(가려움", + "두드러기", + "부종)", + "발적", + "발진", + "발열", + "피부 박리" + ], + "storage": "실온보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "terbiderm_spray_cure_skin_fungal.md" + }, + { + "apc_code": "APC-TIERGARD-TYLOSIN-CON", + "name": "티어가드 정 (타이로신타르타르산)", + "english_name": "Tylosin tartrate", + "manufacturer": "미상", + "category": "마크롤라이드계 항생제 (16원환 Macrolide)", + "target_animal": [ + "개" + ], + "administration": "경구 (oral)", + "ingredients": "Tylosin tartrate → 타일로신으로서 20mg / 60mg / 200mg/정", + "efficacy": "포도상구균, 연쇄상구균에 의한 결막염, 농피증 치료", + "dosage": "", + "precautions": [ + "마크롤라이드 과민반응", + "린코사마이드 병용", + "임신", + "수유", + "고양이", + "일과성 위염", + "장기 투여(>100일) 시 마크롤라이드 내성균 선택 위험" + ], + "storage": "실온보관", + "shelf_life": "제조일로부터 24개월", + "source_file": "tiergard_tylosin_conjunctivitis_tearstain.md" + } +] \ No newline at end of file diff --git a/scripts/parse_markdown.py b/scripts/parse_markdown.py new file mode 100644 index 0000000..74424df --- /dev/null +++ b/scripts/parse_markdown.py @@ -0,0 +1,380 @@ +# -*- coding: utf-8 -*- +""" +동물약 마크다운 -> JSON 변환 파서 +소스: new_anipharm 폴더의 마크다운 파일들 +출력: data/drugs_from_md.json +""" + +import os +import re +import json +from pathlib import Path +from typing import Optional, Dict, List, Any + +# 경로 설정 +SOURCE_DIR = Path(r"C:\Users\청춘약국\source\new_anipharm") +OUTPUT_FILE = Path(r"C:\Users\청춘약국\source\animal-medication-api\data\drugs_from_md.json") + +# 제외할 파일 (제품 정보가 아닌 파일들) +EXCLUDE_FILES = { + "anipharm_renewal_plan.md", + "drug_research_golden_pattern.md", + "esccap_gl_reference.md", + "GL1_5.md", + "site_fullrenewal.md", + "tosspayments_analysis.md", + "antibiotic_reference.md", + "heartworm_tick_combination_question.md", + "metronidazole_amx.md", +} + +# 필드 매핑 (마크다운 표 항목 → JSON 필드) +TABLE_FIELD_MAP = { + "제품명": "name", + "영문명": "english_name", + "제조사": "manufacturer", + "분류": "category", + "약물 계열": "category", + "대상동물": "target_animal", + "대상 동물": "target_animal", + "투여 경로": "administration", + "투여경로": "administration", + "적응증": "efficacy", + "효능효과": "efficacy", + "효능·효과": "efficacy", + "성분": "ingredients", + "성분 (1정)": "ingredients", + "주성분": "ingredients", + "용법용량": "dosage", + "기본 용량": "dosage", + "보관": "storage", + "보관방법": "storage", + "유효기간": "shelf_life", + "금기": "precautions", + "금기사항": "precautions", + "주의사항": "precautions", +} + + +def parse_table(content: str, table_header: str = "제품 정보 요약") -> Dict[str, str]: + """마크다운 표에서 제품 정보 추출""" + result = {} + + # 표 찾기: "## 1. 제품 정보 요약" 또는 "## 제품 정보 요약" 등 + pattern = rf"##\s*\d*\.?\s*{table_header}.*?\n([\s\S]*?)(?=\n##|\n---|\Z)" + match = re.search(pattern, content, re.IGNORECASE) + + if not match: + return result + + table_section = match.group(1) + + # 표 행 파싱: | 항목 | 내용 | + row_pattern = r"\|\s*\*?\*?([^|*]+)\*?\*?\s*\|\s*([^|]+)\s*\|" + rows = re.findall(row_pattern, table_section) + + for key, value in rows: + key = key.strip().replace("**", "") + value = value.strip().replace("**", "") + + # 필드 매핑 + for table_key, json_key in TABLE_FIELD_MAP.items(): + if table_key in key: + result[json_key] = value + break + + return result + + +def extract_json_block(content: str) -> Optional[Dict[str, Any]]: + """DB 저장용 JSON 코드블록 추출""" + # "DB 저장용 JSON" 또는 "제품 메타데이터" 섹션의 첫 번째 JSON 블록 + pattern = r"(?:DB 저장용 JSON|제품 메타데이터).*?```json\s*([\s\S]*?)```" + match = re.search(pattern, content, re.IGNORECASE) + + if match: + json_str = match.group(1).strip() + try: + return json.loads(json_str) + except json.JSONDecodeError: + pass + + return None + + +def extract_apc_code(content: str, filename: str) -> str: + """APC 코드 추출 (없으면 파일명 기반 생성)""" + # 패턴: APC-XXX 또는 apc_code 필드 + patterns = [ + r"APC[_-]?(\d+)", + r'"apc_code":\s*"([^"]+)"', + ] + + for pattern in patterns: + match = re.search(pattern, content, re.IGNORECASE) + if match: + code = match.group(1) + if not code.startswith("APC"): + code = f"APC-{code}" + return code + + # 파일명 기반 생성 + base = Path(filename).stem.upper().replace("_", "-")[:20] + return f"APC-{base}" + + +def parse_target_animal(value: Any) -> List[str]: + """대상 동물 파싱""" + if isinstance(value, list): + return value + + if isinstance(value, str): + # "개, 고양이" 또는 "개·고양이" 또는 "개/고양이" + animals = re.split(r"[,·/\s]+", value) + animals = [a.strip() for a in animals if a.strip()] + + # 일반화 + result = [] + for a in animals: + a_lower = a.lower() + if "개" in a or "dog" in a_lower: + if "개" not in result: + result.append("개") + if "고양이" in a or "cat" in a_lower: + if "고양이" not in result: + result.append("고양이") + + return result if result else ["개", "고양이"] + + return ["개", "고양이"] + + +def parse_precautions(value: Any) -> List[str]: + """주의사항/금기사항 파싱""" + if isinstance(value, list): + return value + + if isinstance(value, str): + # 여러 항목 분리 + items = re.split(r"[;,·]|\n", value) + return [item.strip() for item in items if item.strip()] + + return [] + + +def extract_title_name(content: str) -> tuple[str, str]: + """마크다운 제목에서 제품명과 영문명 추출""" + # 첫 번째 # 제목 찾기 + match = re.search(r"^#\s*(.+?)(?:\s*[-–—]\s*|\n)", content, re.MULTILINE) + if match: + title = match.group(1).strip() + # 괄호 안 영문명 추출 + eng_match = re.search(r"\(([A-Za-z][A-Za-z\s\-®]+)\)", title) + eng_name = eng_match.group(1) if eng_match else "" + # 제품명 정리 (괄호 앞 부분) + name = re.sub(r"\s*\([^)]+\)\s*", "", title).strip() + return name, eng_name + return "", "" + + +def normalize_drug(data: Dict[str, Any], filename: str, content: str) -> Dict[str, Any]: + """약품 데이터 정규화 및 기본값 적용""" + + # 제목에서 제품명 추출 시도 + title_name, title_eng = extract_title_name(content) + + result = { + "apc_code": extract_apc_code(content, filename), + "name": title_name or "미정", + "english_name": title_eng or "", + "manufacturer": "미상", + "category": "", + "target_animal": ["개", "고양이"], + "administration": "경구", + "ingredients": "", + "efficacy": "", + "dosage": "", + "precautions": [], + "storage": "실온보관", + "shelf_life": "제조일로부터 24개월", + "source_file": filename, + } + + # 데이터 병합 + for key, value in data.items(): + if key == "product_name": + result["name"] = value + elif key == "product_name_en": + result["english_name"] = value + elif key == "generic_name": + if not result["english_name"]: + result["english_name"] = value + elif key == "drug_class": + result["category"] = value + elif key == "target_animals": + result["target_animal"] = parse_target_animal(value) + elif key == "target_animal": + result["target_animal"] = parse_target_animal(value) + elif key == "route": + result["administration"] = value + elif key == "composition_per_tablet": + if isinstance(value, dict): + parts = [] + for k, v in value.items(): + if "mg" in k: + parts.append(f"{k.replace('_mg', '')}: {v}mg") + if parts: + result["ingredients"] = ", ".join(parts) + elif key == "dosing_single": + if isinstance(value, dict): + result["dosage"] = "; ".join(f"{k}: {v}" for k, v in value.items()) + else: + result["dosage"] = str(value) + elif key == "contraindication": + result["precautions"] = parse_precautions(value) + elif key == "side_effects": + existing = result.get("precautions", []) + result["precautions"] = existing + parse_precautions(value) + elif key == "storage": + result["storage"] = value + elif key == "manufacturer": + result["manufacturer"] = value + elif key in result: + result[key] = value + + # 이름에서 영문명 추출 시도 + if result["name"] != "미정" and not result["english_name"]: + match = re.search(r"\(([A-Za-z][A-Za-z\s]+)\)", result["name"]) + if match: + result["english_name"] = match.group(1) + + return result + + +def extract_any_json_block(content: str) -> Optional[Dict[str, Any]]: + """문서 내 첫 번째 product_name 포함 JSON 블록 추출""" + # 모든 ```json 블록 찾기 + pattern = r"```json\s*([\s\S]*?)```" + matches = re.findall(pattern, content) + + for json_str in matches: + try: + data = json.loads(json_str.strip()) + # dict이고 product_name이 있으면 반환 + if isinstance(data, dict) and "product_name" in data: + return data + except json.JSONDecodeError: + continue + + return None + + +def parse_markdown_file(filepath: Path) -> Optional[Dict[str, Any]]: + """단일 마크다운 파일 파싱""" + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + except Exception as e: + print(f" ❌ 파일 읽기 실패: {filepath.name} - {e}") + return None + + # 제품 정보인지 확인 (제목에 제품명이 있거나, 제품 정보 요약 표가 있는 경우) + if "제품 정보 요약" not in content and "DB 저장용 JSON" not in content: + print(f" ⏭️ 제품 정보 없음: {filepath.name}") + return None + + # 1. 표에서 추출 + table_data = parse_table(content) + + # 2. JSON 블록에서 추출 (두 가지 방법 시도) + json_data = extract_json_block(content) + if not json_data: + json_data = extract_any_json_block(content) + json_data = json_data or {} + + # 3. 병합 (JSON 우선) + merged = {**table_data, **json_data} + + # 4. 정규화 (데이터가 없어도 제목에서 추출 가능) + result = normalize_drug(merged, filepath.name, content) + + # 최소 조건: 이름이 "미정"이 아니거나 JSON 데이터가 있는 경우 + if result["name"] == "미정" and not json_data: + print(f" ⏭️ 파싱 데이터 없음: {filepath.name}") + return None + + return result + + +def main(): + """메인 실행""" + print("=" * 60) + print("동물약 마크다운 → JSON 변환 파서") + print("=" * 60) + print(f"소스: {SOURCE_DIR}") + print(f"출력: {OUTPUT_FILE}") + print() + + # 마크다운 파일 목록 + md_files = list(SOURCE_DIR.glob("*.md")) + print(f"총 {len(md_files)}개 마크다운 파일 발견") + print() + + drugs = [] + success = 0 + skipped = 0 + failed = 0 + + for filepath in sorted(md_files): + # 제외 파일 확인 + if filepath.name in EXCLUDE_FILES: + print(f" ⏭️ 제외: {filepath.name}") + skipped += 1 + continue + + print(f"📄 처리 중: {filepath.name}") + + result = parse_markdown_file(filepath) + + if result: + drugs.append(result) + print(f" ✅ 성공: {result['name']}") + success += 1 + else: + failed += 1 + + # 중복 제거 (이름 기준) + seen_names = set() + unique_drugs = [] + for drug in drugs: + name = drug["name"] + if name not in seen_names: + seen_names.add(name) + unique_drugs.append(drug) + else: + print(f" ⚠️ 중복 제거: {name}") + + # 결과 저장 + OUTPUT_FILE.parent.mkdir(parents=True, exist_ok=True) + with open(OUTPUT_FILE, "w", encoding="utf-8") as f: + json.dump(unique_drugs, f, ensure_ascii=False, indent=2) + + print() + print("=" * 60) + print("변환 완료") + print("=" * 60) + print(f"✅ 성공: {success}개") + print(f"⏭️ 스킵: {skipped}개") + print(f"❌ 실패: {failed}개") + print(f"📦 최종 약품 수: {len(unique_drugs)}개") + print(f"💾 저장: {OUTPUT_FILE}") + + # 변환된 약품 목록 출력 + print() + print("변환된 약품 목록:") + for i, drug in enumerate(unique_drugs, 1): + print(f" {i}. {drug['name']} ({drug['english_name'] or 'N/A'})") + + +if __name__ == "__main__": + main() diff --git a/test_real_data.py b/test_real_data.py new file mode 100644 index 0000000..2963f2c --- /dev/null +++ b/test_real_data.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +실제 마크다운 변환 데이터로 PDF 테스트 +""" + +import os +import sys +import json +sys.path.insert(0, os.path.dirname(__file__)) + +from animal_med.renderer import AnimalMedRenderer + + +def main(): + print("=" * 60) + print("실제 동물약 데이터 PDF 테스트") + print("=" * 60) + + # 변환된 JSON 로드 + json_path = os.path.join(os.path.dirname(__file__), 'data', 'drugs_from_md.json') + with open(json_path, 'r', encoding='utf-8') as f: + drugs_list = json.load(f) + + print(f"\n총 약품 수: {len(drugs_list)}") + + # drugs 딕셔너리로 변환 (apc_code를 키로) + drugs_dict = {drug['apc_code']: drug for drug in drugs_list} + + # 렌더러 생성 (커스텀 데이터 사용) + renderer = AnimalMedRenderer() + renderer.drugs = drugs_dict # 데이터 교체 + + # 테스트할 약품 3개 선택 + test_codes = list(drugs_dict.keys())[:3] + + print(f"\n테스트 약품:") + for code in test_codes: + drug = drugs_dict[code] + print(f" - {drug['name']} ({drug['category']})") + + # PDF 렌더링 + output_dir = os.path.join(os.path.dirname(__file__), 'output') + os.makedirs(output_dir, exist_ok=True) + + pdf_path = os.path.join(output_dir, 'real_data_test.pdf') + + print(f"\n[PDF 렌더링 중...]") + + result = renderer.render_to_pdf( + apc_codes=test_codes, + output_path=pdf_path, + patient_name="박보호자", + pet_name="몽이", + pet_species="말티즈", + pet_age="5세" + ) + + 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") + + 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()