Files
animal-medication-api/docs/AI_MAPPING_ARCHITECTURE.md
청춘약국 dab2ecae44 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>
2026-04-06 18:07:12 +09:00

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 매핑 필요 ⚠️

왜 어려운가?

  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)

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. 내 생각 (용림)

현실적인 접근

  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 인터페이스 (안)

요청

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)
  • 매핑 실패 로깅 & 대시보드
  • 피드백 루프 (오매핑 수정)

이 문서는 논의용입니다. 피드백 주세요! 🐉