버전: 1.0
작성일: 2026-03-06
목표: 약국 재고 관리 및 주문을 AI가 학습하여 완전 자동화
"약사님이 주문에 신경 쓰지 않아도 되는 약국"
AI가 사용량, 재고, 도매상 상황, 과거 주문 패턴을 학습하여: - 언제 주문할지 - 어느 도매상에 주문할지 - 어떤 규격으로 주문할지 - 얼마나 주문할지
모든 것을 자동으로 결정하고 실행합니다.
| AS-IS | TO-BE |
|---|---|
| 매일 재고 확인 | AI가 자동 모니터링 |
| 수동으로 도매상 선택 | AI가 최적 도매상 선택 |
| 경험에 의존한 주문량 | 데이터 기반 최적 주문량 |
| 주문 누락/지연 발생 | 선제적 자동 주문 |
학습 데이터:
- 약품별 과거 주문 규격 (30T, 100T, 300T, 500T)
- 각 규격 선택 시점의 재고/사용량
- 선택 결과 (남은 재고, 다음 주문까지 기간)
학습 목표:
- 사용량 대비 최적 규격 예측
- 낭비 최소화 (유통기한 고려)
- 단가 최적화 (대용량 할인 vs 소량 회전)
예시 시나리오: | 사용량/월 | 학습된 최적 규격 | 이유 | |-----------|-----------------|------| | 50개 | 30T x 2 | 소량, 빠른 회전 | | 200개 | 100T x 2 | 중간, 적정 재고 | | 800개 | 300T x 3 | 대량, 단가 절감 |
학습 데이터:
- 주문 시점의 재고 수준
- 재고 소진까지 남은 일수
- 주문 후 입고까지 리드타임
- 품절 발생 이력
학습 목표:
- 약사님의 재고 선호도 파악
- 타이트형: 최소 재고 유지 (현금 흐름 중시)
- 여유형: 안전 재고 확보 (품절 방지 중시)
재고 전략 프로파일:
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. 정확 매칭형: 사용량 = 주문량
2. 안전 마진형: 사용량 + α
3. 라운드업형: 규격 단위로 올림
4. 할인 최적형: MOQ(최소주문량) 충족
학습 데이터:
- 도매상별 주문 빈도
- 도매상별 가격
- 도매상별 재고 상황
- 도매상별 배송 속도
- 분할 주문 패턴
학습 목표:
- 기본 도매상 선호도
- 상황별 대체 도매상
- 분할 주문 조건
도매상 선택 로직:
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])
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
);
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)
);
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 추천 │ │ • 승인 요청 │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
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
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
}
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'])
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
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)
}
주문 실행 → 결과 기록 → 평가 → 학습 → 모델 업데이트
│ │
└────────────────────────────────────────┘
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(...)
}
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']
)
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원
[승인] [수정] [거절]
| 지표 | 현재 | 목표 |
|---|---|---|
| 주문 소요 시간 | 30분/일 | 0분 (자동) |
| 품절 발생률 | 5% | <1% |
| 재고 회전율 | - | +20% |
| 주문 비용 절감 | - | 5-10% |
| 폐기 손실 | - | -30% |
"AI는 약사님의 주문 습관을 학습합니다."
AI는 대체하는 것이 아니라, 약사님의 방식을 자동화합니다.
docs/GEOYOUNG_API_REVERSE_ENGINEERING.mdbackend/order_db.pydocs/RX_USAGE_GEOYOUNG_GUIDE.md