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:
347
docs/AI_MAPPING_ARCHITECTURE.md
Normal file
347
docs/AI_MAPPING_ARCHITECTURE.md
Normal 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
226
docs/API_SPEC.md
Normal 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 | 최초 배포 |
|
||||
Reference in New Issue
Block a user