- 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>
348 lines
14 KiB
Markdown
348 lines
14 KiB
Markdown
# 동물약 투약지도서 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 매핑 필요** ⚠️ |
|
|
|
|
### 왜 어려운가?
|
|
|
|
1. **약국마다 바코드 체계 다름**
|
|
- A약국: "안텔민사향" → 바코드 "A001"
|
|
- B약국: "안텔민사향" → 바코드 "B999"
|
|
- C약국: "안텔민사향" → 바코드 없음 (수기 입력)
|
|
|
|
2. **제품명 표기 불일치**
|
|
- POS: "안텔민뽀삐(5kg이하)"
|
|
- APDB: "뉴펫 안텔민 정사 정 100mg/25mg/10정"
|
|
- 쇼핑몰: "안텔민 뽀삐 5kg이하"
|
|
|
|
3. **사이즈/용량 구분**
|
|
- 하트세이버 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)
|
|
|
|
```python
|
|
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: 퍼지 매칭 + 점수 기반
|
|
|
|
```python
|
|
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 하이브리드 (권장) ⭐
|
|
|
|
```python
|
|
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. 내 생각 (용림)
|
|
|
|
### 현실적인 접근
|
|
|
|
1. **Phase 1: 규칙 기반 시작**
|
|
- 현재 17건의 APC 매핑된 제품은 직접 조회
|
|
- 자주 쓰는 10~20개 제품은 수동 규칙 추가
|
|
- 나머지는 "매핑 실패" 로깅
|
|
|
|
2. **Phase 2: 퍼지 매칭 도입**
|
|
- PostgreSQL `pg_trgm` 확장으로 유사도 검색
|
|
- 95% 이상 매칭은 자동 처리
|
|
- 70~95%는 로깅 + 수동 검토
|
|
|
|
3. **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 인터페이스 (안)
|
|
|
|
### 요청
|
|
|
|
```json
|
|
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세"
|
|
}
|
|
```
|
|
|
|
### 응답
|
|
|
|
```json
|
|
{
|
|
"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)
|
|
- [ ] 매핑 실패 로깅 & 대시보드
|
|
- [ ] 피드백 루프 (오매핑 수정)
|
|
|
|
---
|
|
|
|
*이 문서는 논의용입니다. 피드백 주세요!* 🐉
|