- 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 - 테스트 스크립트들
1072 lines
36 KiB
HTML
1072 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ko">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>스마트헬스케어 사업제안서</title>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@400;500;600;700&display=swap');
|
||
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Noto Sans KR', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
line-height: 1.8;
|
||
color: #1e293b;
|
||
max-width: 210mm;
|
||
margin: 0 auto;
|
||
padding: 20mm;
|
||
background: #fff;
|
||
}
|
||
|
||
h1 {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
color: #6366f1;
|
||
margin: 40px 0 20px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 3px solid #6366f1;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #334155;
|
||
margin: 35px 0 15px;
|
||
padding-bottom: 8px;
|
||
border-bottom: 2px solid #e2e8f0;
|
||
}
|
||
|
||
h3 {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #475569;
|
||
margin: 25px 0 12px;
|
||
}
|
||
|
||
h4 {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #64748b;
|
||
margin: 20px 0 10px;
|
||
}
|
||
|
||
p {
|
||
margin: 12px 0;
|
||
text-align: justify;
|
||
}
|
||
|
||
blockquote {
|
||
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
|
||
border-left: 4px solid #6366f1;
|
||
padding: 16px 20px;
|
||
margin: 20px 0;
|
||
border-radius: 0 8px 8px 0;
|
||
font-style: italic;
|
||
color: #475569;
|
||
}
|
||
|
||
code {
|
||
background: #f1f5f9;
|
||
padding: 2px 6px;
|
||
border-radius: 4px;
|
||
font-family: 'Consolas', 'Monaco', monospace;
|
||
font-size: 13px;
|
||
color: #dc2626;
|
||
}
|
||
|
||
pre {
|
||
background: #1e293b;
|
||
color: #e2e8f0;
|
||
padding: 20px;
|
||
border-radius: 12px;
|
||
overflow-x: auto;
|
||
margin: 20px 0;
|
||
font-size: 12px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
pre code {
|
||
background: none;
|
||
color: inherit;
|
||
padding: 0;
|
||
}
|
||
|
||
table {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
margin: 20px 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
th {
|
||
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
|
||
color: #fff;
|
||
padding: 12px 16px;
|
||
text-align: left;
|
||
font-weight: 600;
|
||
}
|
||
|
||
td {
|
||
padding: 12px 16px;
|
||
border-bottom: 1px solid #e2e8f0;
|
||
}
|
||
|
||
tr:nth-child(even) {
|
||
background: #f8fafc;
|
||
}
|
||
|
||
ul, ol {
|
||
margin: 15px 0;
|
||
padding-left: 25px;
|
||
}
|
||
|
||
li {
|
||
margin: 8px 0;
|
||
}
|
||
|
||
hr {
|
||
border: none;
|
||
height: 2px;
|
||
background: linear-gradient(90deg, #6366f1, #8b5cf6, #ec4899);
|
||
margin: 40px 0;
|
||
border-radius: 2px;
|
||
}
|
||
|
||
strong {
|
||
color: #334155;
|
||
font-weight: 600;
|
||
}
|
||
|
||
em {
|
||
color: #64748b;
|
||
}
|
||
|
||
/* 첫 페이지 타이틀 */
|
||
h1:first-of-type {
|
||
font-size: 32px;
|
||
text-align: center;
|
||
border-bottom: none;
|
||
margin-top: 60px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
h1:first-of-type + blockquote {
|
||
text-align: center;
|
||
border-left: none;
|
||
background: none;
|
||
font-size: 18px;
|
||
margin-bottom: 60px;
|
||
}
|
||
|
||
/* 프린트 스타일 */
|
||
@media print {
|
||
body {
|
||
padding: 15mm;
|
||
}
|
||
|
||
pre {
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
}
|
||
|
||
h1, h2, h3 {
|
||
page-break-after: avoid;
|
||
}
|
||
|
||
table, pre, blockquote {
|
||
page-break-inside: avoid;
|
||
}
|
||
}
|
||
|
||
/* 페이지 구분 */
|
||
.page-break {
|
||
page-break-before: always;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1 id="ai-erp">AI ERP 자동 주문 시스템 기획서</h1>
|
||
<blockquote>
|
||
<p>버전: 1.0<br />
|
||
작성일: 2026-03-06<br />
|
||
목표: 약국 재고 관리 및 주문을 AI가 학습하여 완전 자동화</p>
|
||
</blockquote>
|
||
<hr />
|
||
<h2 id="executive-summary">📋 Executive Summary</h2>
|
||
<h3 id="_1">비전</h3>
|
||
<p><strong>"약사님이 주문에 신경 쓰지 않아도 되는 약국"</strong></p>
|
||
<p>AI가 사용량, 재고, 도매상 상황, 과거 주문 패턴을 학습하여:
|
||
- 언제 주문할지
|
||
- 어느 도매상에 주문할지
|
||
- 어떤 규격으로 주문할지
|
||
- 얼마나 주문할지</p>
|
||
<p>모든 것을 자동으로 결정하고 실행합니다.</p>
|
||
<h3 id="_2">핵심 가치</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>AS-IS</th>
|
||
<th>TO-BE</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>매일 재고 확인</td>
|
||
<td>AI가 자동 모니터링</td>
|
||
</tr>
|
||
<tr>
|
||
<td>수동으로 도매상 선택</td>
|
||
<td>AI가 최적 도매상 선택</td>
|
||
</tr>
|
||
<tr>
|
||
<td>경험에 의존한 주문량</td>
|
||
<td>데이터 기반 최적 주문량</td>
|
||
</tr>
|
||
<tr>
|
||
<td>주문 누락/지연 발생</td>
|
||
<td>선제적 자동 주문</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<hr />
|
||
<h2 id="_3">🎯 시스템 목표</h2>
|
||
<h3 id="1">1차 목표 (자동화)</h3>
|
||
<ul>
|
||
<li>[ ] 재고 부족 품목 자동 감지</li>
|
||
<li>[ ] 도매상 자동 선택 및 주문</li>
|
||
<li>[ ] 주문 결과 자동 피드백</li>
|
||
</ul>
|
||
<h3 id="2">2차 목표 (최적화)</h3>
|
||
<ul>
|
||
<li>[ ] 비용 최소화 (가격, 배송비)</li>
|
||
<li>[ ] 재고 최적화 (과잉/부족 방지)</li>
|
||
<li>[ ] 주문 타이밍 최적화</li>
|
||
</ul>
|
||
<h3 id="3">3차 목표 (예측)</h3>
|
||
<ul>
|
||
<li>[ ] 수요 예측 (계절, 요일, 이벤트)</li>
|
||
<li>[ ] 공급 리스크 예측 (품절, 단종)</li>
|
||
<li>[ ] 가격 변동 예측</li>
|
||
</ul>
|
||
<hr />
|
||
<h2 id="ai">🧠 AI 학습 요소</h2>
|
||
<h3 id="1_1">1. 주문 패턴 학습</h3>
|
||
<h4 id="11-spec-selection">1.1 규격 선택 패턴 (Spec Selection)</h4>
|
||
<pre><code>학습 데이터:
|
||
- 약품별 과거 주문 규격 (30T, 100T, 300T, 500T)
|
||
- 각 규격 선택 시점의 재고/사용량
|
||
- 선택 결과 (남은 재고, 다음 주문까지 기간)
|
||
|
||
학습 목표:
|
||
- 사용량 대비 최적 규격 예측
|
||
- 낭비 최소화 (유통기한 고려)
|
||
- 단가 최적화 (대용량 할인 vs 소량 회전)
|
||
</code></pre>
|
||
<p><strong>예시 시나리오:</strong>
|
||
| 사용량/월 | 학습된 최적 규격 | 이유 |
|
||
|-----------|-----------------|------|
|
||
| 50개 | 30T x 2 | 소량, 빠른 회전 |
|
||
| 200개 | 100T x 2 | 중간, 적정 재고 |
|
||
| 800개 | 300T x 3 | 대량, 단가 절감 |</p>
|
||
<h4 id="12-inventory-strategy">1.2 재고 전략 학습 (Inventory Strategy)</h4>
|
||
<pre><code>학습 데이터:
|
||
- 주문 시점의 재고 수준
|
||
- 재고 소진까지 남은 일수
|
||
- 주문 후 입고까지 리드타임
|
||
- 품절 발생 이력
|
||
|
||
학습 목표:
|
||
- 약사님의 재고 선호도 파악
|
||
- 타이트형: 최소 재고 유지 (현금 흐름 중시)
|
||
- 여유형: 안전 재고 확보 (품절 방지 중시)
|
||
</code></pre>
|
||
<p><strong>재고 전략 프로파일:</strong></p>
|
||
<pre><code class="language-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
|
||
}
|
||
</code></pre>
|
||
<h4 id="13-order-quantity">1.3 주문량 전략 학습 (Order Quantity)</h4>
|
||
<pre><code>학습 데이터:
|
||
- 사용량 (일별, 주별, 월별)
|
||
- 주문량
|
||
- 주문 후 소진까지 기간
|
||
- 사용량 변동성 (표준편차)
|
||
|
||
학습 패턴:
|
||
1. 정확 매칭형: 사용량 = 주문량
|
||
2. 안전 마진형: 사용량 + α
|
||
3. 라운드업형: 규격 단위로 올림
|
||
4. 할인 최적형: MOQ(최소주문량) 충족
|
||
</code></pre>
|
||
<h4 id="14-wholesaler-selection">1.4 도매상 선택 학습 (Wholesaler Selection)</h4>
|
||
<pre><code>학습 데이터:
|
||
- 도매상별 주문 빈도
|
||
- 도매상별 가격
|
||
- 도매상별 재고 상황
|
||
- 도매상별 배송 속도
|
||
- 분할 주문 패턴
|
||
|
||
학습 목표:
|
||
- 기본 도매상 선호도
|
||
- 상황별 대체 도매상
|
||
- 분할 주문 조건
|
||
</code></pre>
|
||
<p><strong>도매상 선택 로직:</strong></p>
|
||
<pre><code class="language-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])
|
||
</code></pre>
|
||
<hr />
|
||
<h2 id="_4">📊 데이터 모델</h2>
|
||
<h3 id="ai_1">주문 컨텍스트 (AI 학습용)</h3>
|
||
<pre><code class="language-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
|
||
);
|
||
</code></pre>
|
||
<h3 id="_5">사용량 시계열</h3>
|
||
<pre><code class="language-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)
|
||
);
|
||
</code></pre>
|
||
<h3 id="ai_2">AI 분석 결과</h3>
|
||
<pre><code class="language-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
|
||
);
|
||
</code></pre>
|
||
<hr />
|
||
<h2 id="_6">🔄 시스템 아키텍처</h2>
|
||
<h3 id="_7">전체 흐름</h3>
|
||
<pre><code>┌─────────────────────────────────────────────────────────────────┐
|
||
│ AI ERP 자동 주문 시스템 │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
│
|
||
┌───────────────────────┼───────────────────────┐
|
||
▼ ▼ ▼
|
||
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
|
||
│ 데이터 수집 │ │ AI 분석 │ │ 자동 실행 │
|
||
│ │ │ │ │ │
|
||
│ • POS 판매 │─────▶│ • 사용량 예측 │─────▶│ • 도매상 API │
|
||
│ • 처방전 조제 │ │ • 재고 분석 │ │ • 주문 실행 │
|
||
│ • 현재 재고 │ │ • 주문 추천 │ │ • 결과 피드백 │
|
||
│ • 도매상 재고 │ │ • 패턴 학습 │ │ │
|
||
└───────────────┘ └───────────────┘ └───────────────┘
|
||
│ │ │
|
||
└───────────────────────┼───────────────────────┘
|
||
▼
|
||
┌───────────────────┐
|
||
│ 학습 루프 │
|
||
│ │
|
||
│ 주문 결과 평가 │
|
||
│ → 모델 업데이트 │
|
||
│ → 전략 조정 │
|
||
└───────────────────┘
|
||
</code></pre>
|
||
<h3 id="_8">컴포넌트 상세</h3>
|
||
<pre><code>┌──────────────────────────────────────────────────────────────────┐
|
||
│ 데이터 레이어 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
|
||
│ │ PIT3000 │ │ SQLite │ │ 지오영 │ │ 수인 │ │
|
||
│ │ (MSSQL) │ │ Orders DB │ │ API │ │ API │ │
|
||
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
|
||
│ │ │ │ │ │
|
||
│ └───────────────┴───────────────┴───────────────┘ │
|
||
│ │ │
|
||
└────────────────────────────────┼─────────────────────────────────┘
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ 서비스 레이어 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ InventorySync │ │ UsageAnalyzer │ │ OrderExecutor │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ • 재고 동기화 │ │ • 사용량 집계 │ │ • 주문 실행 │ │
|
||
│ │ • 실시간 추적 │ │ • 트렌드 분석 │ │ • 결과 처리 │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ AIPredictor │ │ AIOptimizer │ │ AILearner │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ • 수요 예측 │ │ • 규격 최적화 │ │ • 패턴 학습 │ │
|
||
│ │ • 재고 예측 │ │ • 도매상 선택 │ │ • 모델 업데이트 │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ 인터페이스 레이어 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||
│ │ 웹 대시보드 │ │ 알림 시스템 │ │ 관리자 앱 │ │
|
||
│ │ │ │ │ │ │ │
|
||
│ │ • 재고 현황 │ │ • 주문 알림 │ │ • 수동 개입 │ │
|
||
│ │ • 주문 이력 │ │ • 이상 감지 │ │ • 설정 조정 │ │
|
||
│ │ • AI 추천 │ │ • 승인 요청 │ │ │ │
|
||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||
│ │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
</code></pre>
|
||
<hr />
|
||
<h2 id="ai_3">🤖 AI 모델 설계</h2>
|
||
<h3 id="1_2">1. 수요 예측 모델</h3>
|
||
<pre><code class="language-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
|
||
</code></pre>
|
||
<h3 id="2_1">2. 재고 최적화 모델</h3>
|
||
<pre><code class="language-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
|
||
}
|
||
</code></pre>
|
||
<h3 id="3_1">3. 규격 선택 모델</h3>
|
||
<pre><code class="language-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'])
|
||
</code></pre>
|
||
<h3 id="4">4. 도매상 선택 모델</h3>
|
||
<pre><code class="language-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
|
||
</code></pre>
|
||
<h3 id="5">5. 주문 결정 엔진</h3>
|
||
<pre><code class="language-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)
|
||
}
|
||
</code></pre>
|
||
<hr />
|
||
<h2 id="_9">📈 학습 파이프라인</h2>
|
||
<h3 id="_10">피드백 루프</h3>
|
||
<pre><code>주문 실행 → 결과 기록 → 평가 → 학습 → 모델 업데이트
|
||
│ │
|
||
└────────────────────────────────────────┘
|
||
</code></pre>
|
||
<h3 id="_11">평가 지표</h3>
|
||
<pre><code class="language-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(...)
|
||
}
|
||
</code></pre>
|
||
<h3 id="_12">모델 업데이트</h3>
|
||
<pre><code class="language-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']
|
||
)
|
||
</code></pre>
|
||
<hr />
|
||
<h2 id="_13">⚙️ 자동화 레벨</h2>
|
||
<h3 id="level-0">Level 0: 수동</h3>
|
||
<ul>
|
||
<li>AI 추천만 제공</li>
|
||
<li>모든 주문은 수동 실행</li>
|
||
</ul>
|
||
<h3 id="level-1">Level 1: 반자동</h3>
|
||
<ul>
|
||
<li>AI가 주문 계획 생성</li>
|
||
<li>약사님 승인 후 자동 실행</li>
|
||
<li>알림: 승인 요청</li>
|
||
</ul>
|
||
<h3 id="level-2">Level 2: 조건부 자동</h3>
|
||
<ul>
|
||
<li>신뢰도 높은 주문은 자동 실행</li>
|
||
<li>신뢰도 낮은 주문만 승인 요청</li>
|
||
<li>조건 예시:</li>
|
||
<li>자주 주문하는 품목</li>
|
||
<li>금액 임계값 이하</li>
|
||
<li>긴급하지 않은 주문</li>
|
||
</ul>
|
||
<h3 id="level-3">Level 3: 완전 자동</h3>
|
||
<ul>
|
||
<li>모든 주문 자동 실행</li>
|
||
<li>이상 상황만 알림</li>
|
||
<li>약사님은 대시보드로 모니터링</li>
|
||
</ul>
|
||
<pre><code class="language-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)
|
||
</code></pre>
|
||
<hr />
|
||
<h2 id="_14">🔔 알림 시스템</h2>
|
||
<h3 id="_15">알림 유형</h3>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>유형</th>
|
||
<th>조건</th>
|
||
<th>채널</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>승인 요청</td>
|
||
<td>Level 1-2에서 자동 실행 안 되는 주문</td>
|
||
<td>카톡, 앱 푸시</td>
|
||
</tr>
|
||
<tr>
|
||
<td>주문 완료</td>
|
||
<td>자동 주문 실행됨</td>
|
||
<td>앱 푸시</td>
|
||
</tr>
|
||
<tr>
|
||
<td>재고 경고</td>
|
||
<td>안전 재고 이하</td>
|
||
<td>카톡</td>
|
||
</tr>
|
||
<tr>
|
||
<td>품절 긴급</td>
|
||
<td>재고 0, 당일 필요</td>
|
||
<td>전화, 카톡</td>
|
||
</tr>
|
||
<tr>
|
||
<td>이상 감지</td>
|
||
<td>비정상 사용량, 가격 급등</td>
|
||
<td>앱 푸시</td>
|
||
</tr>
|
||
<tr>
|
||
<td>일간 리포트</td>
|
||
<td>매일 오전</td>
|
||
<td>이메일</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<h3 id="_16">알림 메시지 예시</h3>
|
||
<pre><code>📦 주문 승인 요청
|
||
|
||
약품: 콩코르정 2.5mg
|
||
현재고: 45개 (3일치)
|
||
추천 주문: 300T x 2박스
|
||
도매상: 지오영
|
||
예상 금액: 72,000원
|
||
|
||
[승인] [수정] [거절]
|
||
</code></pre>
|
||
<hr />
|
||
<h2 id="_17">📅 개발 로드맵</h2>
|
||
<h3 id="phase-1-1-2">Phase 1: 기반 구축 (1-2주)</h3>
|
||
<ul>
|
||
<li>[x] 지오영 API 연동</li>
|
||
<li>[x] 주문 DB 스키마 설계</li>
|
||
<li>[x] 주문 컨텍스트 로깅</li>
|
||
<li>[ ] 수인 API 연동</li>
|
||
<li>[ ] 일별 사용량 집계 자동화</li>
|
||
</ul>
|
||
<h3 id="phase-2-ai-2-3">Phase 2: AI 기본 (2-3주)</h3>
|
||
<ul>
|
||
<li>[ ] 수요 예측 모델 (단순 이동평균)</li>
|
||
<li>[ ] 재주문점 계산</li>
|
||
<li>[ ] 규격 선택 로직 (규칙 기반)</li>
|
||
<li>[ ] 도매상 선택 로직 (규칙 기반)</li>
|
||
<li>[ ] 주문 추천 대시보드</li>
|
||
</ul>
|
||
<h3 id="phase-3-2-3">Phase 3: 학습 시스템 (2-3주)</h3>
|
||
<ul>
|
||
<li>[ ] 피드백 루프 구현</li>
|
||
<li>[ ] 주문 평가 시스템</li>
|
||
<li>[ ] 패턴 학습 (규격, 도매상)</li>
|
||
<li>[ ] 재고 전략 프로파일링</li>
|
||
</ul>
|
||
<h3 id="phase-4-1-2">Phase 4: 자동화 (1-2주)</h3>
|
||
<ul>
|
||
<li>[ ] Level 1 (승인 후 자동)</li>
|
||
<li>[ ] 알림 시스템 연동</li>
|
||
<li>[ ] Level 2 (조건부 자동)</li>
|
||
<li>[ ] 모니터링 대시보드</li>
|
||
</ul>
|
||
<h3 id="phase-5">Phase 5: 고도화 (지속)</h3>
|
||
<ul>
|
||
<li>[ ] ML 모델 적용 (XGBoost, LSTM)</li>
|
||
<li>[ ] Level 3 (완전 자동)</li>
|
||
<li>[ ] 다중 약국 지원</li>
|
||
<li>[ ] 수요 예측 정교화</li>
|
||
</ul>
|
||
<hr />
|
||
<h2 id="kpi">📊 성공 지표 (KPI)</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>지표</th>
|
||
<th>현재</th>
|
||
<th>목표</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>주문 소요 시간</td>
|
||
<td>30분/일</td>
|
||
<td>0분 (자동)</td>
|
||
</tr>
|
||
<tr>
|
||
<td>품절 발생률</td>
|
||
<td>5%</td>
|
||
<td><1%</td>
|
||
</tr>
|
||
<tr>
|
||
<td>재고 회전율</td>
|
||
<td>-</td>
|
||
<td>+20%</td>
|
||
</tr>
|
||
<tr>
|
||
<td>주문 비용 절감</td>
|
||
<td>-</td>
|
||
<td>5-10%</td>
|
||
</tr>
|
||
<tr>
|
||
<td>폐기 손실</td>
|
||
<td>-</td>
|
||
<td>-30%</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<hr />
|
||
<h2 id="_18">🔐 보안 및 안전장치</h2>
|
||
<h3 id="_19">자동 주문 제한</h3>
|
||
<ul>
|
||
<li>일일 자동 주문 금액 상한</li>
|
||
<li>단일 품목 최대 수량</li>
|
||
<li>신규 품목 자동 주문 제외</li>
|
||
<li>가격 급등 시 수동 전환</li>
|
||
</ul>
|
||
<h3 id="_20">롤백 메커니즘</h3>
|
||
<ul>
|
||
<li>모든 주문 취소 가능 (확정 전)</li>
|
||
<li>자동화 레벨 즉시 변경</li>
|
||
<li>긴급 수동 모드 전환</li>
|
||
</ul>
|
||
<h3 id="_21">감사 로그</h3>
|
||
<ul>
|
||
<li>모든 AI 결정 기록</li>
|
||
<li>자동 실행 이력</li>
|
||
<li>승인/거절 이력</li>
|
||
</ul>
|
||
<hr />
|
||
<h2 id="_22">💡 핵심 인사이트</h2>
|
||
<blockquote>
|
||
<p>"AI는 약사님의 주문 습관을 학습합니다."</p>
|
||
</blockquote>
|
||
<ul>
|
||
<li>약사님이 항상 지오영에 먼저 주문하면 → AI도 지오영 우선</li>
|
||
<li>약사님이 300T보다 30T를 선호하면 → AI도 소량 주문</li>
|
||
<li>약사님이 여유 있게 주문하면 → AI도 안전 재고 확보</li>
|
||
<li>약사님이 가격에 민감하면 → AI도 최저가 추적</li>
|
||
</ul>
|
||
<p><strong>AI는 대체하는 것이 아니라, 약사님의 방식을 자동화합니다.</strong></p>
|
||
<hr />
|
||
<h2 id="_23">📚 참고 자료</h2>
|
||
<ul>
|
||
<li>지오영 API 문서: <code>docs/GEOYOUNG_API_REVERSE_ENGINEERING.md</code></li>
|
||
<li>주문 DB 스키마: <code>backend/order_db.py</code></li>
|
||
<li>사용량 조회 페이지: <code>docs/RX_USAGE_GEOYOUNG_GUIDE.md</code></li>
|
||
</ul>
|
||
</body>
|
||
</html> |