feat: 도매상 API 통합 및 스키마 업데이트
- wholesale 패키지 연동 (SooinSession, GeoYoungSession) - Flask Blueprint 분리 (sooin_api.py, geoyoung_api.py) - order_context 스키마 확장 (wholesaler_id, internal_code 등) - 수인약품 개별 취소 기능 (cancel_item, restore_item) - 문서 추가: WHOLESALE_API_INTEGRATION.md - 테스트 스크립트들
This commit is contained in:
875
docs/AI_ERP_AUTO_ORDER_SYSTEM.md
Normal file
875
docs/AI_ERP_AUTO_ORDER_SYSTEM.md
Normal file
@@ -0,0 +1,875 @@
|
||||
# AI ERP 자동 주문 시스템 기획서
|
||||
|
||||
> 버전: 1.0
|
||||
> 작성일: 2026-03-06
|
||||
> 목표: 약국 재고 관리 및 주문을 AI가 학습하여 완전 자동화
|
||||
|
||||
---
|
||||
|
||||
## 📋 Executive Summary
|
||||
|
||||
### 비전
|
||||
**"약사님이 주문에 신경 쓰지 않아도 되는 약국"**
|
||||
|
||||
AI가 사용량, 재고, 도매상 상황, 과거 주문 패턴을 학습하여:
|
||||
- 언제 주문할지
|
||||
- 어느 도매상에 주문할지
|
||||
- 어떤 규격으로 주문할지
|
||||
- 얼마나 주문할지
|
||||
|
||||
모든 것을 자동으로 결정하고 실행합니다.
|
||||
|
||||
### 핵심 가치
|
||||
| AS-IS | TO-BE |
|
||||
|-------|-------|
|
||||
| 매일 재고 확인 | AI가 자동 모니터링 |
|
||||
| 수동으로 도매상 선택 | AI가 최적 도매상 선택 |
|
||||
| 경험에 의존한 주문량 | 데이터 기반 최적 주문량 |
|
||||
| 주문 누락/지연 발생 | 선제적 자동 주문 |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 시스템 목표
|
||||
|
||||
### 1차 목표 (자동화)
|
||||
- [ ] 재고 부족 품목 자동 감지
|
||||
- [ ] 도매상 자동 선택 및 주문
|
||||
- [ ] 주문 결과 자동 피드백
|
||||
|
||||
### 2차 목표 (최적화)
|
||||
- [ ] 비용 최소화 (가격, 배송비)
|
||||
- [ ] 재고 최적화 (과잉/부족 방지)
|
||||
- [ ] 주문 타이밍 최적화
|
||||
|
||||
### 3차 목표 (예측)
|
||||
- [ ] 수요 예측 (계절, 요일, 이벤트)
|
||||
- [ ] 공급 리스크 예측 (품절, 단종)
|
||||
- [ ] 가격 변동 예측
|
||||
|
||||
---
|
||||
|
||||
## 🧠 AI 학습 요소
|
||||
|
||||
### 1. 주문 패턴 학습
|
||||
|
||||
#### 1.1 규격 선택 패턴 (Spec Selection)
|
||||
```
|
||||
학습 데이터:
|
||||
- 약품별 과거 주문 규격 (30T, 100T, 300T, 500T)
|
||||
- 각 규격 선택 시점의 재고/사용량
|
||||
- 선택 결과 (남은 재고, 다음 주문까지 기간)
|
||||
|
||||
학습 목표:
|
||||
- 사용량 대비 최적 규격 예측
|
||||
- 낭비 최소화 (유통기한 고려)
|
||||
- 단가 최적화 (대용량 할인 vs 소량 회전)
|
||||
```
|
||||
|
||||
**예시 시나리오:**
|
||||
| 사용량/월 | 학습된 최적 규격 | 이유 |
|
||||
|-----------|-----------------|------|
|
||||
| 50개 | 30T x 2 | 소량, 빠른 회전 |
|
||||
| 200개 | 100T x 2 | 중간, 적정 재고 |
|
||||
| 800개 | 300T x 3 | 대량, 단가 절감 |
|
||||
|
||||
#### 1.2 재고 전략 학습 (Inventory Strategy)
|
||||
|
||||
```
|
||||
학습 데이터:
|
||||
- 주문 시점의 재고 수준
|
||||
- 재고 소진까지 남은 일수
|
||||
- 주문 후 입고까지 리드타임
|
||||
- 품절 발생 이력
|
||||
|
||||
학습 목표:
|
||||
- 약사님의 재고 선호도 파악
|
||||
- 타이트형: 최소 재고 유지 (현금 흐름 중시)
|
||||
- 여유형: 안전 재고 확보 (품절 방지 중시)
|
||||
```
|
||||
|
||||
**재고 전략 프로파일:**
|
||||
```python
|
||||
class InventoryStrategy:
|
||||
TIGHT = {
|
||||
'safety_days': 2, # 안전 재고 2일치
|
||||
'reorder_point': 0.8, # 80% 소진 시 주문
|
||||
'order_coverage': 7 # 7일치 주문
|
||||
}
|
||||
|
||||
MODERATE = {
|
||||
'safety_days': 5,
|
||||
'reorder_point': 0.6,
|
||||
'order_coverage': 14
|
||||
}
|
||||
|
||||
CONSERVATIVE = {
|
||||
'safety_days': 10,
|
||||
'reorder_point': 0.5,
|
||||
'order_coverage': 30
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 주문량 전략 학습 (Order Quantity)
|
||||
|
||||
```
|
||||
학습 데이터:
|
||||
- 사용량 (일별, 주별, 월별)
|
||||
- 주문량
|
||||
- 주문 후 소진까지 기간
|
||||
- 사용량 변동성 (표준편차)
|
||||
|
||||
학습 패턴:
|
||||
1. 정확 매칭형: 사용량 = 주문량
|
||||
2. 안전 마진형: 사용량 + α
|
||||
3. 라운드업형: 규격 단위로 올림
|
||||
4. 할인 최적형: MOQ(최소주문량) 충족
|
||||
```
|
||||
|
||||
#### 1.4 도매상 선택 학습 (Wholesaler Selection)
|
||||
|
||||
```
|
||||
학습 데이터:
|
||||
- 도매상별 주문 빈도
|
||||
- 도매상별 가격
|
||||
- 도매상별 재고 상황
|
||||
- 도매상별 배송 속도
|
||||
- 분할 주문 패턴
|
||||
|
||||
학습 목표:
|
||||
- 기본 도매상 선호도
|
||||
- 상황별 대체 도매상
|
||||
- 분할 주문 조건
|
||||
```
|
||||
|
||||
**도매상 선택 로직:**
|
||||
```python
|
||||
def select_wholesaler(product, quantity, urgency):
|
||||
"""
|
||||
AI가 학습한 도매상 선택 로직
|
||||
|
||||
고려 요소:
|
||||
1. 재고 (있는 곳 우선)
|
||||
2. 가격 (저렴한 곳)
|
||||
3. 선호도 (과거 패턴)
|
||||
4. 긴급도 (배송 속도)
|
||||
"""
|
||||
candidates = []
|
||||
|
||||
for ws in wholesalers:
|
||||
score = 0
|
||||
|
||||
# 재고 체크
|
||||
if ws.has_stock(product, quantity):
|
||||
score += 100
|
||||
|
||||
# 가격 (낮을수록 높은 점수)
|
||||
score += (1 - ws.price_ratio) * 50
|
||||
|
||||
# 학습된 선호도
|
||||
score += ai_model.preference_score(ws, product) * 30
|
||||
|
||||
# 긴급도 반영
|
||||
if urgency == 'high':
|
||||
score += ws.delivery_speed * 20
|
||||
|
||||
candidates.append((ws, score))
|
||||
|
||||
return max(candidates, key=lambda x: x[1])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 데이터 모델
|
||||
|
||||
### 주문 컨텍스트 (AI 학습용)
|
||||
|
||||
```sql
|
||||
CREATE TABLE order_context (
|
||||
id INTEGER PRIMARY KEY,
|
||||
order_item_id INTEGER,
|
||||
|
||||
-- 약품 정보
|
||||
drug_code TEXT,
|
||||
product_name TEXT,
|
||||
|
||||
-- 주문 시점 상황
|
||||
stock_at_order INTEGER, -- 주문 시점 재고
|
||||
usage_1d INTEGER, -- 최근 1일 사용량
|
||||
usage_7d INTEGER, -- 최근 7일 사용량
|
||||
usage_30d INTEGER, -- 최근 30일 사용량
|
||||
avg_daily_usage REAL, -- 일평균 사용량
|
||||
usage_stddev REAL, -- 사용량 변동성
|
||||
|
||||
-- 주문 결정
|
||||
ordered_spec TEXT, -- 선택한 규격 (30T, 300T)
|
||||
ordered_qty INTEGER, -- 주문 수량
|
||||
ordered_dose INTEGER, -- 총 정제수
|
||||
wholesaler_id TEXT, -- 선택한 도매상
|
||||
|
||||
-- 선택지 정보
|
||||
available_specs JSON, -- 가능했던 규격들
|
||||
available_wholesalers JSON, -- 가능했던 도매상들
|
||||
spec_stocks JSON, -- 규격별 재고
|
||||
wholesaler_prices JSON, -- 도매상별 가격
|
||||
|
||||
-- 선택 이유 (AI 분석용)
|
||||
selection_reason TEXT, -- 'price', 'stock', 'preference', 'urgency'
|
||||
|
||||
-- 예측 vs 실제
|
||||
predicted_days_coverage REAL, -- 예상 커버 일수
|
||||
actual_days_to_reorder INT, -- 실제 재주문까지 일수
|
||||
|
||||
-- 결과 평가
|
||||
was_optimal BOOLEAN, -- 최적 선택이었나
|
||||
waste_amount INTEGER, -- 낭비량 (폐기, 유통기한)
|
||||
stockout_occurred BOOLEAN, -- 품절 발생했나
|
||||
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### 사용량 시계열
|
||||
|
||||
```sql
|
||||
CREATE TABLE daily_usage (
|
||||
id INTEGER PRIMARY KEY,
|
||||
drug_code TEXT,
|
||||
usage_date DATE,
|
||||
|
||||
-- 출처별 사용량
|
||||
rx_qty INTEGER, -- 처방전 사용량
|
||||
pos_qty INTEGER, -- POS 판매량
|
||||
return_qty INTEGER, -- 반품량
|
||||
|
||||
-- 집계
|
||||
net_usage INTEGER, -- 순 사용량
|
||||
|
||||
-- 재고 스냅샷
|
||||
stock_start INTEGER,
|
||||
stock_end INTEGER,
|
||||
|
||||
-- 특이사항
|
||||
is_holiday BOOLEAN,
|
||||
is_event BOOLEAN, -- 프로모션 등
|
||||
weather TEXT, -- 날씨 (선택)
|
||||
|
||||
UNIQUE(drug_code, usage_date)
|
||||
);
|
||||
```
|
||||
|
||||
### AI 분석 결과
|
||||
|
||||
```sql
|
||||
CREATE TABLE ai_recommendations (
|
||||
id INTEGER PRIMARY KEY,
|
||||
drug_code TEXT,
|
||||
analysis_date DATE,
|
||||
|
||||
-- 현재 상황
|
||||
current_stock INTEGER,
|
||||
avg_daily_usage REAL,
|
||||
days_of_stock REAL,
|
||||
|
||||
-- AI 추천
|
||||
should_order BOOLEAN,
|
||||
recommended_qty INTEGER,
|
||||
recommended_spec TEXT,
|
||||
recommended_wholesaler TEXT,
|
||||
urgency_level TEXT, -- 'low', 'medium', 'high', 'critical'
|
||||
|
||||
-- 추천 근거
|
||||
reasoning JSON,
|
||||
confidence_score REAL,
|
||||
|
||||
-- 실행 상태
|
||||
auto_executed BOOLEAN,
|
||||
executed_at TIMESTAMP,
|
||||
execution_result TEXT,
|
||||
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 시스템 아키텍처
|
||||
|
||||
### 전체 흐름
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ AI ERP 자동 주문 시스템 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────┼───────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||||
│ 데이터 수집 │ │ AI 분석 │ │ 자동 실행 │
|
||||
│ │ │ │ │ │
|
||||
│ • POS 판매 │─────▶│ • 사용량 예측 │─────▶│ • 도매상 API │
|
||||
│ • 처방전 조제 │ │ • 재고 분석 │ │ • 주문 실행 │
|
||||
│ • 현재 재고 │ │ • 주문 추천 │ │ • 결과 피드백 │
|
||||
│ • 도매상 재고 │ │ • 패턴 학습 │ │ │
|
||||
└───────────────┘ └───────────────┘ └───────────────┘
|
||||
│ │ │
|
||||
└───────────────────────┼───────────────────────┘
|
||||
▼
|
||||
┌───────────────────┐
|
||||
│ 학습 루프 │
|
||||
│ │
|
||||
│ 주문 결과 평가 │
|
||||
│ → 모델 업데이트 │
|
||||
│ → 전략 조정 │
|
||||
└───────────────────┘
|
||||
```
|
||||
|
||||
### 컴포넌트 상세
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 데이터 레이어 │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ PIT3000 │ │ SQLite │ │ 지오영 │ │ 수인 │ │
|
||||
│ │ (MSSQL) │ │ Orders DB │ │ API │ │ API │ │
|
||||
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ └───────────────┴───────────────┴───────────────┘ │
|
||||
│ │ │
|
||||
└────────────────────────────────┼─────────────────────────────────┘
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 서비스 레이어 │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ InventorySync │ │ UsageAnalyzer │ │ OrderExecutor │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 재고 동기화 │ │ • 사용량 집계 │ │ • 주문 실행 │ │
|
||||
│ │ • 실시간 추적 │ │ • 트렌드 분석 │ │ • 결과 처리 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ AIPredictor │ │ AIOptimizer │ │ AILearner │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 수요 예측 │ │ • 규격 최적화 │ │ • 패턴 학습 │ │
|
||||
│ │ • 재고 예측 │ │ • 도매상 선택 │ │ • 모델 업데이트 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ 인터페이스 레이어 │
|
||||
├──────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 웹 대시보드 │ │ 알림 시스템 │ │ 관리자 앱 │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • 재고 현황 │ │ • 주문 알림 │ │ • 수동 개입 │ │
|
||||
│ │ • 주문 이력 │ │ • 이상 감지 │ │ • 설정 조정 │ │
|
||||
│ │ • AI 추천 │ │ • 승인 요청 │ │ │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI 모델 설계
|
||||
|
||||
### 1. 수요 예측 모델
|
||||
|
||||
```python
|
||||
class DemandPredictor:
|
||||
"""
|
||||
약품별 일간 수요 예측
|
||||
|
||||
입력:
|
||||
- 과거 30일 사용량
|
||||
- 요일 (월~일)
|
||||
- 계절/월
|
||||
- 특수일 (공휴일, 이벤트)
|
||||
|
||||
출력:
|
||||
- 향후 7일 예측 사용량
|
||||
- 예측 신뢰구간
|
||||
"""
|
||||
|
||||
def predict(self, drug_code: str, days: int = 7) -> dict:
|
||||
features = self._extract_features(drug_code)
|
||||
|
||||
prediction = {
|
||||
'daily_forecast': [], # 일별 예측
|
||||
'total_forecast': 0, # 총 예측량
|
||||
'confidence': 0.0, # 신뢰도
|
||||
'lower_bound': 0, # 하한
|
||||
'upper_bound': 0 # 상한
|
||||
}
|
||||
|
||||
return prediction
|
||||
```
|
||||
|
||||
### 2. 재고 최적화 모델
|
||||
|
||||
```python
|
||||
class InventoryOptimizer:
|
||||
"""
|
||||
최적 재고 수준 및 재주문점 계산
|
||||
|
||||
입력:
|
||||
- 예측 수요
|
||||
- 리드타임 (주문~입고)
|
||||
- 서비스 수준 (품절 허용률)
|
||||
- 재고 유지 비용
|
||||
|
||||
출력:
|
||||
- 재주문점 (Reorder Point)
|
||||
- 안전 재고 (Safety Stock)
|
||||
- 최적 주문량 (EOQ)
|
||||
"""
|
||||
|
||||
def calculate_reorder_point(self, drug_code: str) -> dict:
|
||||
demand = self.demand_predictor.predict(drug_code)
|
||||
lead_time = self._get_lead_time(drug_code)
|
||||
|
||||
# 재주문점 = 리드타임 수요 + 안전재고
|
||||
lead_time_demand = demand['daily_avg'] * lead_time
|
||||
safety_stock = self._calculate_safety_stock(drug_code)
|
||||
|
||||
return {
|
||||
'reorder_point': lead_time_demand + safety_stock,
|
||||
'safety_stock': safety_stock,
|
||||
'lead_time_days': lead_time
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 규격 선택 모델
|
||||
|
||||
```python
|
||||
class SpecSelector:
|
||||
"""
|
||||
최적 규격 선택
|
||||
|
||||
고려 요소:
|
||||
- 예상 사용량
|
||||
- 규격별 단가
|
||||
- 유통기한
|
||||
- 과거 선택 패턴
|
||||
"""
|
||||
|
||||
def select_spec(self, drug_code: str, needed_qty: int,
|
||||
available_specs: list) -> dict:
|
||||
|
||||
candidates = []
|
||||
|
||||
for spec in available_specs:
|
||||
spec_qty = self._parse_spec_qty(spec) # "300T" → 300
|
||||
|
||||
# 필요 단위 수 계산
|
||||
units_needed = math.ceil(needed_qty / spec_qty)
|
||||
total_qty = units_needed * spec_qty
|
||||
waste = total_qty - needed_qty
|
||||
|
||||
# 비용 계산
|
||||
unit_price = self._get_unit_price(drug_code, spec)
|
||||
total_cost = units_needed * unit_price
|
||||
cost_per_dose = total_cost / total_qty
|
||||
|
||||
# 학습된 선호도
|
||||
preference = self.ai_model.spec_preference(drug_code, spec)
|
||||
|
||||
# 점수 계산
|
||||
score = self._calculate_score(
|
||||
waste_ratio=waste / total_qty,
|
||||
cost_efficiency=1 / cost_per_dose,
|
||||
preference=preference
|
||||
)
|
||||
|
||||
candidates.append({
|
||||
'spec': spec,
|
||||
'units': units_needed,
|
||||
'total_qty': total_qty,
|
||||
'waste': waste,
|
||||
'cost': total_cost,
|
||||
'score': score
|
||||
})
|
||||
|
||||
return max(candidates, key=lambda x: x['score'])
|
||||
```
|
||||
|
||||
### 4. 도매상 선택 모델
|
||||
|
||||
```python
|
||||
class WholesalerSelector:
|
||||
"""
|
||||
최적 도매상 선택 (다중 도매상 지원)
|
||||
|
||||
고려 요소:
|
||||
- 재고 유무
|
||||
- 가격
|
||||
- 배송 속도
|
||||
- 과거 선호도
|
||||
- 최소 주문 금액
|
||||
"""
|
||||
|
||||
def select_wholesaler(self, drug_code: str, spec: str,
|
||||
quantity: int, urgency: str) -> dict:
|
||||
|
||||
wholesalers = ['geoyoung', 'sooin', 'baekje']
|
||||
candidates = []
|
||||
|
||||
for ws in wholesalers:
|
||||
# 재고 확인
|
||||
stock = self._check_stock(ws, drug_code, spec)
|
||||
if stock < quantity:
|
||||
continue
|
||||
|
||||
# 가격 조회
|
||||
price = self._get_price(ws, drug_code, spec)
|
||||
|
||||
# 배송 속도
|
||||
delivery_hours = self._get_delivery_time(ws)
|
||||
|
||||
# AI 학습 선호도
|
||||
preference = self.ai_model.wholesaler_preference(
|
||||
drug_code, ws
|
||||
)
|
||||
|
||||
# 종합 점수
|
||||
score = self._calculate_score(
|
||||
has_stock=True,
|
||||
price=price,
|
||||
delivery=delivery_hours,
|
||||
preference=preference,
|
||||
urgency=urgency
|
||||
)
|
||||
|
||||
candidates.append({
|
||||
'wholesaler': ws,
|
||||
'stock': stock,
|
||||
'price': price,
|
||||
'delivery_hours': delivery_hours,
|
||||
'score': score
|
||||
})
|
||||
|
||||
if not candidates:
|
||||
return self._handle_no_stock(drug_code, spec, quantity)
|
||||
|
||||
return max(candidates, key=lambda x: x['score'])
|
||||
|
||||
def _handle_no_stock(self, drug_code, spec, quantity):
|
||||
"""재고 없을 때: 분할 주문 또는 대체품"""
|
||||
# 1. 다른 규격으로 분할
|
||||
# 2. 다중 도매상 분할
|
||||
# 3. 대체 약품 추천
|
||||
pass
|
||||
```
|
||||
|
||||
### 5. 주문 결정 엔진
|
||||
|
||||
```python
|
||||
class OrderDecisionEngine:
|
||||
"""
|
||||
종합 주문 결정
|
||||
|
||||
매일 실행:
|
||||
1. 모든 약품 재고 스캔
|
||||
2. 재주문점 도달 품목 식별
|
||||
3. 각 품목별 최적 주문 계획 수립
|
||||
4. 자동 실행 또는 승인 요청
|
||||
"""
|
||||
|
||||
def daily_analysis(self) -> list:
|
||||
recommendations = []
|
||||
|
||||
for drug in self._get_all_drugs():
|
||||
current_stock = self._get_stock(drug.code)
|
||||
reorder_point = self.inventory_optimizer.calculate_reorder_point(drug.code)
|
||||
|
||||
if current_stock <= reorder_point['reorder_point']:
|
||||
# 주문 필요
|
||||
order_plan = self._create_order_plan(drug)
|
||||
recommendations.append(order_plan)
|
||||
|
||||
return recommendations
|
||||
|
||||
def _create_order_plan(self, drug) -> dict:
|
||||
# 1. 필요 수량 계산
|
||||
needed_qty = self._calculate_needed_qty(drug)
|
||||
|
||||
# 2. 최적 규격 선택
|
||||
spec = self.spec_selector.select_spec(
|
||||
drug.code, needed_qty, drug.available_specs
|
||||
)
|
||||
|
||||
# 3. 최적 도매상 선택
|
||||
wholesaler = self.wholesaler_selector.select_wholesaler(
|
||||
drug.code, spec['spec'], spec['units'],
|
||||
urgency=self._determine_urgency(drug)
|
||||
)
|
||||
|
||||
return {
|
||||
'drug_code': drug.code,
|
||||
'drug_name': drug.name,
|
||||
'current_stock': self._get_stock(drug.code),
|
||||
'needed_qty': needed_qty,
|
||||
'recommended_spec': spec['spec'],
|
||||
'recommended_units': spec['units'],
|
||||
'recommended_wholesaler': wholesaler['wholesaler'],
|
||||
'estimated_cost': wholesaler['price'] * spec['units'],
|
||||
'urgency': self._determine_urgency(drug),
|
||||
'confidence': self._calculate_confidence(),
|
||||
'auto_execute': self._should_auto_execute(drug)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 학습 파이프라인
|
||||
|
||||
### 피드백 루프
|
||||
|
||||
```
|
||||
주문 실행 → 결과 기록 → 평가 → 학습 → 모델 업데이트
|
||||
│ │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 평가 지표
|
||||
|
||||
```python
|
||||
class OrderEvaluator:
|
||||
"""주문 결과 평가"""
|
||||
|
||||
def evaluate(self, order_id: int) -> dict:
|
||||
order = self._get_order(order_id)
|
||||
|
||||
# 1. 재고 효율성
|
||||
days_covered = self._calculate_days_covered(order)
|
||||
expected_days = order.expected_coverage
|
||||
coverage_accuracy = days_covered / expected_days
|
||||
|
||||
# 2. 비용 효율성
|
||||
actual_cost_per_dose = order.total_cost / order.total_dose
|
||||
market_avg_cost = self._get_market_avg_cost(order.drug_code)
|
||||
cost_efficiency = market_avg_cost / actual_cost_per_dose
|
||||
|
||||
# 3. 낭비율
|
||||
waste = self._calculate_waste(order)
|
||||
waste_ratio = waste / order.total_dose
|
||||
|
||||
# 4. 품절 발생 여부
|
||||
stockout = self._check_stockout_before_next_order(order)
|
||||
|
||||
return {
|
||||
'coverage_accuracy': coverage_accuracy,
|
||||
'cost_efficiency': cost_efficiency,
|
||||
'waste_ratio': waste_ratio,
|
||||
'stockout_occurred': stockout,
|
||||
'overall_score': self._calculate_overall_score(...)
|
||||
}
|
||||
```
|
||||
|
||||
### 모델 업데이트
|
||||
|
||||
```python
|
||||
class AILearner:
|
||||
"""주문 결과로부터 학습"""
|
||||
|
||||
def learn_from_order(self, order_id: int):
|
||||
evaluation = self.evaluator.evaluate(order_id)
|
||||
context = self._get_order_context(order_id)
|
||||
|
||||
# 1. 규격 선택 학습
|
||||
self.spec_model.update(
|
||||
drug_code=context.drug_code,
|
||||
chosen_spec=context.ordered_spec,
|
||||
was_optimal=evaluation['waste_ratio'] < 0.1
|
||||
)
|
||||
|
||||
# 2. 재고 전략 학습
|
||||
self.inventory_model.update(
|
||||
drug_code=context.drug_code,
|
||||
reorder_point=context.stock_at_order,
|
||||
was_optimal=not evaluation['stockout_occurred']
|
||||
)
|
||||
|
||||
# 3. 도매상 선호도 학습
|
||||
self.wholesaler_model.update(
|
||||
drug_code=context.drug_code,
|
||||
chosen_wholesaler=context.wholesaler_id,
|
||||
satisfaction=evaluation['cost_efficiency']
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 자동화 레벨
|
||||
|
||||
### Level 0: 수동
|
||||
- AI 추천만 제공
|
||||
- 모든 주문은 수동 실행
|
||||
|
||||
### Level 1: 반자동
|
||||
- AI가 주문 계획 생성
|
||||
- 약사님 승인 후 자동 실행
|
||||
- 알림: 승인 요청
|
||||
|
||||
### Level 2: 조건부 자동
|
||||
- 신뢰도 높은 주문은 자동 실행
|
||||
- 신뢰도 낮은 주문만 승인 요청
|
||||
- 조건 예시:
|
||||
- 자주 주문하는 품목
|
||||
- 금액 임계값 이하
|
||||
- 긴급하지 않은 주문
|
||||
|
||||
### Level 3: 완전 자동
|
||||
- 모든 주문 자동 실행
|
||||
- 이상 상황만 알림
|
||||
- 약사님은 대시보드로 모니터링
|
||||
|
||||
```python
|
||||
class AutomationLevel:
|
||||
def should_auto_execute(self, order_plan: dict) -> bool:
|
||||
level = self.settings.automation_level
|
||||
|
||||
if level == 0:
|
||||
return False
|
||||
|
||||
if level == 1:
|
||||
return False # 항상 승인 필요
|
||||
|
||||
if level == 2:
|
||||
# 조건부 자동
|
||||
conditions = [
|
||||
order_plan['confidence'] > 0.9,
|
||||
order_plan['estimated_cost'] < 100000,
|
||||
order_plan['drug_code'] in self.trusted_drugs,
|
||||
order_plan['urgency'] != 'critical'
|
||||
]
|
||||
return all(conditions)
|
||||
|
||||
if level == 3:
|
||||
# 완전 자동 (이상 상황만 제외)
|
||||
return not self._is_anomaly(order_plan)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔔 알림 시스템
|
||||
|
||||
### 알림 유형
|
||||
|
||||
| 유형 | 조건 | 채널 |
|
||||
|------|------|------|
|
||||
| 승인 요청 | Level 1-2에서 자동 실행 안 되는 주문 | 카톡, 앱 푸시 |
|
||||
| 주문 완료 | 자동 주문 실행됨 | 앱 푸시 |
|
||||
| 재고 경고 | 안전 재고 이하 | 카톡 |
|
||||
| 품절 긴급 | 재고 0, 당일 필요 | 전화, 카톡 |
|
||||
| 이상 감지 | 비정상 사용량, 가격 급등 | 앱 푸시 |
|
||||
| 일간 리포트 | 매일 오전 | 이메일 |
|
||||
|
||||
### 알림 메시지 예시
|
||||
|
||||
```
|
||||
📦 주문 승인 요청
|
||||
|
||||
약품: 콩코르정 2.5mg
|
||||
현재고: 45개 (3일치)
|
||||
추천 주문: 300T x 2박스
|
||||
도매상: 지오영
|
||||
예상 금액: 72,000원
|
||||
|
||||
[승인] [수정] [거절]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📅 개발 로드맵
|
||||
|
||||
### Phase 1: 기반 구축 (1-2주)
|
||||
- [x] 지오영 API 연동
|
||||
- [x] 주문 DB 스키마 설계
|
||||
- [x] 주문 컨텍스트 로깅
|
||||
- [ ] 수인 API 연동
|
||||
- [ ] 일별 사용량 집계 자동화
|
||||
|
||||
### Phase 2: AI 기본 (2-3주)
|
||||
- [ ] 수요 예측 모델 (단순 이동평균)
|
||||
- [ ] 재주문점 계산
|
||||
- [ ] 규격 선택 로직 (규칙 기반)
|
||||
- [ ] 도매상 선택 로직 (규칙 기반)
|
||||
- [ ] 주문 추천 대시보드
|
||||
|
||||
### Phase 3: 학습 시스템 (2-3주)
|
||||
- [ ] 피드백 루프 구현
|
||||
- [ ] 주문 평가 시스템
|
||||
- [ ] 패턴 학습 (규격, 도매상)
|
||||
- [ ] 재고 전략 프로파일링
|
||||
|
||||
### Phase 4: 자동화 (1-2주)
|
||||
- [ ] Level 1 (승인 후 자동)
|
||||
- [ ] 알림 시스템 연동
|
||||
- [ ] Level 2 (조건부 자동)
|
||||
- [ ] 모니터링 대시보드
|
||||
|
||||
### Phase 5: 고도화 (지속)
|
||||
- [ ] ML 모델 적용 (XGBoost, LSTM)
|
||||
- [ ] Level 3 (완전 자동)
|
||||
- [ ] 다중 약국 지원
|
||||
- [ ] 수요 예측 정교화
|
||||
|
||||
---
|
||||
|
||||
## 📊 성공 지표 (KPI)
|
||||
|
||||
| 지표 | 현재 | 목표 |
|
||||
|------|------|------|
|
||||
| 주문 소요 시간 | 30분/일 | 0분 (자동) |
|
||||
| 품절 발생률 | 5% | <1% |
|
||||
| 재고 회전율 | - | +20% |
|
||||
| 주문 비용 절감 | - | 5-10% |
|
||||
| 폐기 손실 | - | -30% |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 보안 및 안전장치
|
||||
|
||||
### 자동 주문 제한
|
||||
- 일일 자동 주문 금액 상한
|
||||
- 단일 품목 최대 수량
|
||||
- 신규 품목 자동 주문 제외
|
||||
- 가격 급등 시 수동 전환
|
||||
|
||||
### 롤백 메커니즘
|
||||
- 모든 주문 취소 가능 (확정 전)
|
||||
- 자동화 레벨 즉시 변경
|
||||
- 긴급 수동 모드 전환
|
||||
|
||||
### 감사 로그
|
||||
- 모든 AI 결정 기록
|
||||
- 자동 실행 이력
|
||||
- 승인/거절 이력
|
||||
|
||||
---
|
||||
|
||||
## 💡 핵심 인사이트
|
||||
|
||||
> "AI는 약사님의 주문 습관을 학습합니다."
|
||||
|
||||
- 약사님이 항상 지오영에 먼저 주문하면 → AI도 지오영 우선
|
||||
- 약사님이 300T보다 30T를 선호하면 → AI도 소량 주문
|
||||
- 약사님이 여유 있게 주문하면 → AI도 안전 재고 확보
|
||||
- 약사님이 가격에 민감하면 → AI도 최저가 추적
|
||||
|
||||
**AI는 대체하는 것이 아니라, 약사님의 방식을 자동화합니다.**
|
||||
|
||||
---
|
||||
|
||||
## 📚 참고 자료
|
||||
|
||||
- 지오영 API 문서: `docs/GEOYOUNG_API_REVERSE_ENGINEERING.md`
|
||||
- 주문 DB 스키마: `backend/order_db.py`
|
||||
- 사용량 조회 페이지: `docs/RX_USAGE_GEOYOUNG_GUIDE.md`
|
||||
Reference in New Issue
Block a user