- anipharm_api.py: 동물약 PDF 생성 API 추가 - data/master/*.json: 16종 마스터 데이터 업데이트 - templates: medication_guide_v2, 로고 추가 - docs: AI 매핑 아키텍처, API 스펙 문서 - .gitignore: _dev_scripts/, *.db 제외 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
14 KiB
동물약 투약지도서 API - AI 매핑 아키텍처
작성일: 2026-03-19
작성자: 용림 🐉
1. 문제 정의
현재 상황
약국 POS (PIT3000)
└─ DrugCode: LB000003158
└─ GoodsName: "안텔민뽀삐(5kg이하)"
└─ BARCODE: 9990000001134 (자체 바코드) ❌ APC 아님
└─ CD_ITEM_UNIT_MEMBER
└─ 0230237010107 (APC) ✅ 있으면 좋음
└─ 9990000001134 (자체) ← 이것만 있는 경우 多
핵심 문제
| 상황 | 비율 | 처리 방법 |
|---|---|---|
| APC 매핑 완료 | ~18% (17건) | APC로 바로 조회 ✅ |
| APC 미매핑 | ~82% (32건+) | 제품명 기반 AI 매핑 필요 ⚠️ |
왜 어려운가?
-
약국마다 바코드 체계 다름
- A약국: "안텔민사향" → 바코드 "A001"
- B약국: "안텔민사향" → 바코드 "B999"
- C약국: "안텔민사향" → 바코드 없음 (수기 입력)
-
제품명 표기 불일치
- POS: "안텔민뽀삐(5kg이하)"
- APDB: "뉴펫 안텔민 정사 정 100mg/25mg/10정"
- 쇼핑몰: "안텔민 뽀삐 5kg이하"
-
사이즈/용량 구분
- 하트세이버 mini/S/M/L → 각각 다른 APC
- 넥스가드 XS/S/M/L → 각각 다른 APC
2. 제안 아키텍처
2.1 전체 흐름
┌─────────────────────────────────────────────────────────────┐
│ API 요청 │
│ { │
│ "items": [ │
│ { "apc": "0230237010107", "name": "안텔민뽀삐" }, │ ← APC 있음
│ { "apc": null, "name": "하트세이버S(5.6~11kg)" } │ ← APC 없음
│ ], │
│ "patient_name": "김남곤", │
│ "pet_name": "뽀삐" │
│ } │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 매핑 분기 처리 │
├─────────────────────────────────────────────────────────────┤
│ │
│ for each item: │
│ if item.apc exists: │
│ ──────────────────▶ [직접 조회] │
│ else: │
│ ──────────────────▶ [AI 매핑 레이어] │
│ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PDF 렌더링 │
│ │
│ 매핑된 APC들로 APDB 조회 → 템플릿 렌더링 → PDF 반환 │
└─────────────────────────────────────────────────────────────┘
2.2 AI 매핑 레이어 상세
┌─────────────────────────────────────────────────────────────┐
│ AI 매핑 레이어 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 입력: "하트세이버S(5.6~11kg)" │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Step 1: 정규화 (Normalize) │ │
│ │ - 공백/특수문자 정리 │ │
│ │ - 체중 정보 추출: 5.6~11kg → S사이즈 │ │
│ │ - 브랜드 추출: "하트세이버" │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Step 2: 후보 검색 (Candidate Search) │ │
│ │ - PostgreSQL 퍼지 매칭 │ │
│ │ - 벡터 유사도 검색 (선택적) │ │
│ │ │ │
│ │ 결과: [ │ │
│ │ { apc: "0230474210202", name: "하트세이버S", score: 0.95 }, │
│ │ { apc: "0230474220200", name: "하트세이버M", score: 0.72 }, │
│ │ { apc: "0230470000008", name: "하트세이버mini", score: 0.68 }│
│ │ ] │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Step 3: AI 최종 선택 (LLM Decision) │ │
│ │ │ │
│ │ 프롬프트: │ │
│ │ "POS 제품명: 하트세이버S(5.6~11kg) │ │
│ │ 후보 APC: │ │
│ │ 1. 0230474210202 - 하트세이버S (5.6~11kg) │ │
│ │ 2. 0230474220200 - 하트세이버M (12~22kg) │ │
│ │ 3. 0230470000008 - 하트세이버mini (5.6kg이하) │ │
│ │ │ │
│ │ 가장 적합한 APC는?" │ │
│ │ │ │
│ │ AI 응답: "0230474210202" (체중 범위 일치) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Step 4: 캐싱 (선택적) │ │
│ │ - 동일 제품명 재요청 시 캐시 사용 │ │
│ │ - Redis 또는 SQLite │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
3. 구현 옵션 비교
옵션 A: 규칙 기반 매핑 (No AI)
def match_by_rules(product_name):
# 정규식 + 키워드 매칭
if "안텔민" in product_name:
if "뽀삐" in product_name or "5kg이하" in product_name:
return "0230237010107"
elif "킹" in product_name or "5kg이상" in product_name:
return "0230237810109"
# ... 수십 개 규칙
| 장점 | 단점 |
|---|---|
| 빠름, 비용 0 | 유지보수 지옥 |
| 예측 가능 | 새 제품 대응 불가 |
| 표기 변형에 취약 |
적합: 제품 수 적고, 변동 없을 때
옵션 B: 퍼지 매칭 + 점수 기반
from rapidfuzz import fuzz
def match_by_fuzzy(product_name, candidates):
scores = []
for c in candidates:
score = fuzz.token_sort_ratio(product_name, c['name'])
scores.append((c['apc'], score))
best = max(scores, key=lambda x: x[1])
if best[1] > 80: # 임계값
return best[0]
return None # 매칭 실패
| 장점 | 단점 |
|---|---|
| AI 비용 없음 | 체중/사이즈 구분 어려움 |
| 빠름 | 애매한 경우 오매핑 |
적합: 1:1 매핑이 명확한 경우
옵션 C: AI 하이브리드 (권장) ⭐
def match_hybrid(product_name):
# 1. 캐시 확인
cached = cache.get(product_name)
if cached:
return cached
# 2. 퍼지 매칭으로 후보 추림
candidates = fuzzy_search(product_name, limit=5)
# 3. 고신뢰 매칭이면 바로 반환
if candidates[0]['score'] > 95:
return candidates[0]['apc']
# 4. 애매하면 AI 판단
if candidates[0]['score'] > 70:
apc = llm_decide(product_name, candidates)
cache.set(product_name, apc)
return apc
# 5. 전혀 못 찾으면 실패
return None
| 장점 | 단점 |
|---|---|
| 정확도 높음 | AI 비용 (캐싱으로 최소화) |
| 새 제품 대응 가능 | 초기 구축 복잡 |
| 체중/사이즈 정확 구분 |
적합: 현재 상황 (다양한 표기, 사이즈 구분 필요)
4. 비용 분석
AI 호출 비용 (GPT-4o-mini 기준)
| 시나리오 | 월 요청 | AI 호출률 | AI 호출 수 | 비용 |
|---|---|---|---|---|
| 소규모 | 100건 | 30% | 30건 | ~$0.01 |
| 중규모 | 1,000건 | 20% | 200건 | ~$0.10 |
| 대규모 | 10,000건 | 10% | 1,000건 | ~$0.50 |
캐싱 효과: 동일 제품명 재요청 시 AI 호출 안 함 → 호출률 급감
5. 내 생각 (용림)
현실적인 접근
-
Phase 1: 규칙 기반 시작
- 현재 17건의 APC 매핑된 제품은 직접 조회
- 자주 쓰는 10~20개 제품은 수동 규칙 추가
- 나머지는 "매핑 실패" 로깅
-
Phase 2: 퍼지 매칭 도입
- PostgreSQL
pg_trgm확장으로 유사도 검색 - 95% 이상 매칭은 자동 처리
- 70~95%는 로깅 + 수동 검토
- PostgreSQL
-
Phase 3: AI 레이어 추가
- 70~95% 구간에 LLM 판단 도입
- 판단 결과 캐싱
- 오매핑 피드백 루프
왜 AI가 필요한가?
POS: "하트세이버츄어블S(5.6~11kg)"
APDB: "뉴펫 하트세이버 츄어블 소형견용 5.6-11kg"
→ 퍼지 매칭만으로는 "소형견용 = S" 판단 어려움
→ 체중 범위 파싱 + 의미 이해 필요
→ LLM이 "5.6~11kg"와 "5.6-11kg" 같다고 판단 가능
우선순위 제안
| 순위 | 항목 | 이유 |
|---|---|---|
| 1 | APC 직접 매핑 확대 | 가장 정확, 비용 0 |
| 2 | 캐시 레이어 | AI 비용 절감 |
| 3 | 퍼지 매칭 | AI 호출 최소화 |
| 4 | AI 최종 판단 | 애매한 케이스만 |
6. API 인터페이스 (안)
요청
POST /api/guide/pdf
{
"items": [
{
"apc": "0230237010107",
"name": "안텔민뽀삐",
"drugcode": "LB000003158"
},
{
"apc": null,
"name": "하트세이버S(5.6~11kg)",
"drugcode": "LB000003153"
}
],
"patient_name": "김남곤",
"pet_name": "뽀삐",
"pet_species": "푸들",
"pet_age": "3세"
}
응답
{
"success": true,
"pdf_url": "/output/guide_20260319_abc123.pdf",
"mapping_results": [
{
"name": "안텔민뽀삐",
"apc": "0230237010107",
"method": "direct",
"confidence": 1.0
},
{
"name": "하트세이버S(5.6~11kg)",
"apc": "0230474210202",
"method": "ai_matched",
"confidence": 0.92,
"candidates_considered": 3
}
]
}
7. 다음 단계
- APDB 퍼지 검색 인덱스 구축 (
pg_trgm) - AI 매핑 프롬프트 설계
- 캐시 레이어 (Redis/SQLite)
- 매핑 실패 로깅 & 대시보드
- 피드백 루프 (오매핑 수정)
이 문서는 논의용입니다. 피드백 주세요! 🐉