feat: 애니팜 투약지도서 API 및 마스터 데이터 업데이트

- 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>
This commit is contained in:
청춘약국
2026-04-06 18:07:12 +09:00
parent 8a18b530bd
commit dab2ecae44
27 changed files with 3880 additions and 391 deletions

View File

@@ -0,0 +1,347 @@
# 동물약 투약지도서 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)
- [ ] 매핑 실패 로깅 & 대시보드
- [ ] 피드백 루프 (오매핑 수정)
---
*이 문서는 논의용입니다. 피드백 주세요!* 🐉

226
docs/API_SPEC.md Normal file
View File

@@ -0,0 +1,226 @@
# 애니팜 투약지도서 API 명세서
> **Base URL:** `https://ap.0bin.in`
> **Local:** `http://localhost:7002`
> **Version:** 1.0.0
> **Last Updated:** 2026-03-19
---
## 📋 엔드포인트 목록
| Method | Endpoint | 설명 |
|--------|----------|------|
| GET | `/health` | 헬스체크 |
| GET | `/api/products` | 약품 목록 조회 |
| POST | `/api/guide/pdf` | PDF 생성 |
| POST | `/api/guide/preview` | 미리보기 (메타데이터) |
---
## 1. 헬스체크
서버 상태 확인
### Request
```
GET /health
```
### Response
```json
{
"status": "ok",
"service": "anipharm-api",
"timestamp": "2026-03-19T20:01:13.292109"
}
```
---
## 2. 약품 목록 조회
등록된 모든 약품 목록 반환
### Request
```
GET /api/products
```
### Response
```json
{
"success": true,
"count": 21,
"products": [
{
"product_id": "MASTER-001",
"name": "넥스가드 스펙트라",
"category": "antiparasitic",
"category_display": "올인원 구충제"
},
{
"product_id": "MASTER-002",
"name": "아시카프 츄어블정",
"category": "nsaid",
"category_display": "진통소염제 (NSAIDs)"
}
// ... 21개
]
}
```
---
## 3. PDF 생성
투약지도서 PDF 파일 생성 및 다운로드
### Request
```
POST /api/guide/pdf
Content-Type: application/json
```
### Request Body
```json
{
"product_ids": ["MASTER-001", "MASTER-002", "MASTER-005"],
"patient_name": "김남곤",
"pet_name": "뽀삐",
"pet_species": "푸들",
"pet_age": "3세",
"pharmacy_name": "청춘약국 동물약 전문상담",
"pharmacy_tel": "033-481-0384"
}
```
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| product_ids | string[] | ✅ | 약품 ID 배열 |
| patient_name | string | ❌ | 보호자 이름 (기본: "보호자") |
| pet_name | string | ❌ | 반려동물 이름 (기본: "반려동물") |
| pet_species | string | ❌ | 품종 |
| pet_age | string | ❌ | 나이 |
| pharmacy_name | string | ❌ | 약국명 (기본: "청춘약국 동물약 전문상담") |
| pharmacy_tel | string | ❌ | 전화번호 (기본: "033-481-0384") |
### Response
- **Success:** `application/pdf` (PDF 파일 다운로드)
- **Error:**
```json
{
"success": false,
"error": "에러 메시지"
}
```
### cURL 예시
```bash
curl -X POST https://ap.0bin.in/api/guide/pdf \
-H "Content-Type: application/json" \
-d '{
"product_ids": ["MASTER-001", "MASTER-002"],
"patient_name": "김남곤",
"pet_name": "뽀삐",
"pet_species": "푸들",
"pet_age": "3세"
}' \
--output 투약지도서.pdf
```
---
## 4. 미리보기 (메타데이터)
PDF 생성 전 약품 정보 및 예상 페이지 수 확인
### Request
```
POST /api/guide/preview
Content-Type: application/json
```
### Request Body
```json
{
"product_ids": ["MASTER-001", "MASTER-002", "MASTER-005"]
}
```
### Response
```json
{
"success": true,
"drug_count": 3,
"page_count": 1,
"drugs": [
{
"id": "MASTER-001",
"name": "넥스가드 스펙트라",
"category": "antiparasitic",
"has_image": true
},
{
"id": "MASTER-002",
"name": "아시카프 츄어블정",
"category": "nsaid",
"has_image": true
},
{
"id": "MASTER-005",
"name": "하트세이버 츄어블",
"category": "antiparasitic",
"has_image": true
}
]
}
```
---
## 📦 등록된 약품 목록 (21개)
| ID | 이름 | 카테고리 |
|----|------|----------|
| MASTER-001 | 넥스가드 스펙트라 | antiparasitic |
| MASTER-002 | 아시카프 츄어블정 | nsaid |
| MASTER-003 | 아시엔로 50 | antibiotic |
| MASTER-004 | 세레니아 정 | antiemetic |
| MASTER-005 | 하트세이버 츄어블 | antiparasitic |
| MASTER-006 | 프로닐스팟 | antiparasitic |
| MASTER-007 | 오리더밀 | otic |
| MASTER-008 | 터비덤 스프레이 | antifungal |
| MASTER-009 | 클로르헥시딘 샴푸 | topical |
| MASTER-010 | 셀라이트 | antiparasitic |
| MASTER-011 | 멜록시캐시 CH | nsaid |
| MASTER-012 | 복합 개시딘 겔 | topical |
| MASTER-013 | 임팩트액 | antiparasitic |
| MASTER-014 | 안텔민 뽀삐/킹 | antiparasitic |
| MASTER-015 | 티어가드 정 | antibiotic |
| MASTER-016 | 액티벳정 | antibiotic |
| MASTER-017 | 아포퀠 | immunomodulator |
| MASTER-018 | 브라벡토 | antiparasitic |
| MASTER-019 | 심파리카 트리오 | antiparasitic |
| MASTER-020 | 가바펜틴 | analgesic |
| MASTER-021 | 메트로니다졸 | antibiotic |
---
## 🔧 서버 정보
| 항목 | 값 |
|------|-----|
| 프레임워크 | Flask |
| 포트 | 7002 |
| PM2 이름 | anipharm-api |
| PDF 엔진 | WeasyPrint |
| 페이지당 약품 | 4개 |
---
## 📝 변경 이력
| 날짜 | 버전 | 내용 |
|------|------|------|
| 2026-03-19 | 1.0.0 | 최초 배포 |