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:
thug0bin
2026-03-06 11:50:46 +09:00
parent e84eda928a
commit c1596a6d35
53 changed files with 8789 additions and 3 deletions

File diff suppressed because it is too large Load Diff

View 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`

View File

@@ -0,0 +1,375 @@
# 지오영 API 리버스 엔지니어링 가이드
> 작성일: 2026-03-06
> 목적: 지오영 도매상 웹사이트의 내부 API를 분석하여 Playwright 대신 requests로 빠른 주문 시스템 구축
---
## 📋 개요
### 문제점
- **Playwright 방식**: 30초+ 소요 (브라우저 실행 → 로그인 → 검색 → 클릭 → 장바구니)
- **경쟁사**: 훨씬 빠른 주문 처리
### 해결책
- 웹사이트의 **내부 AJAX API**를 분석
- **requests + 세션 쿠키**로 직접 호출
- 결과: **~1초** 주문 완료 (30배 빨라짐!)
---
## 🔍 분석 과정
### 1단계: 인증 쿠키 확인
Playwright로 로그인 후 쿠키 확인:
```python
cookies = await page.context.cookies()
print([c['name'] for c in cookies])
# 출력: ['GEORELAUTH']
```
**핵심 발견**: `GEORELAUTH` 쿠키가 인증 토큰
### 2단계: 네트워크 요청 캡처
```python
page.on('request', lambda req: print(req.url, req.method))
```
**발견된 POST 요청:**
- `/Member/Login` - 로그인
- `/Home/PartialSearchProduct` - 제품 검색
- `/Home/PartialProductCart` - 장바구니 조회
### 3단계: JavaScript 번들 분석
```
https://gwn.geoweb.kr/bundles/order?v=...
https://gwn.geoweb.kr/bundles/order_product_cart?v=...
```
정규식으로 함수/URL 추출:
```python
import re
# 함수 찾기
funcs = re.findall(r'function\s+(Add\w*|Process\w*)\s*\(', content)
# AJAX URL 찾기
urls = re.findall(r'url\s*:\s*["\']([^"\']+)["\']', content)
```
### 4단계: 핵심 함수 발견
**AddCart 함수:**
```javascript
function AddCart(n,t,i){
// ... 유효성 검사 ...
ProcessCart("add", e, i, r); // ← 핵심!
}
```
**ProcessCart 함수:**
```javascript
function ProcessCart(n,t,i,r){
var u = {};
u.productCode = t;
u.moveCode = i;
u.orderQty = r;
jsf_com_GetAjax("/Home/DataCart/" + n, u, "json", ...);
}
```
**발견!**
- 장바구니 API: `POST /Home/DataCart/add`
- 파라미터: `productCode`, `moveCode`, `orderQty`
### 5단계: 주문 확정 API 찾기
HTML에서 폼 분석:
```python
soup = BeautifulSoup(html, 'html.parser')
form = soup.find('form', id='frmSave')
print(form.get('action'))
# 출력: /Home/DataOrder
```
**발견!** 주문 확정 API: `POST /Home/DataOrder`
---
## 🔑 최종 API 명세
### 1. 로그인
```
POST https://gwn.geoweb.kr/Member/Login
Content-Type: application/x-www-form-urlencoded
LoginID=7390&Password=trajet6640
→ 쿠키 'GEORELAUTH' 반환
```
### 2. 제품 검색
```
POST https://gwn.geoweb.kr/Home/PartialSearchProduct
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
srchText=661700390
→ HTML 테이블 반환 (보험코드, 제품명, 재고 등)
```
### 3. 장바구니 추가 ⭐
```
POST https://gwn.geoweb.kr/Home/DataCart/add
Content-Type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
productCode=008709 ← 내부 코드 (보험코드 아님!)
moveCode=
orderQty=2
→ {"result": 1, "msg": ""} (성공)
→ {"result": -100, "msg": "주문 등록을 할수없는 제품"} (실패)
```
### 4. 주문 확정 ⭐
```
POST https://gwn.geoweb.kr/Home/DataOrder
Content-Type: application/x-www-form-urlencoded
p_desc=메모
→ 리다이렉트 또는 성공 페이지
```
### 5. 장바구니 비우기
```
POST https://gwn.geoweb.kr/Home/DataCart/delAll
→ 성공 시 200
```
---
## ⚠️ 주의사항 (삽질 포인트)
### 1. productCode ≠ 보험코드
**실수:**
```python
# ❌ 보험코드로 장바구니 추가 시도
session.post('/Home/DataCart/add', data={
'productCode': '661700390', # 보험코드
'orderQty': 1
})
# 결과: {"result": -100, "msg": "주문 등록을 할수없는 제품"}
```
**해결:**
```python
# ✅ 검색 결과에서 내부 코드 추출
soup = BeautifulSoup(search_html, 'html.parser')
product_div = soup.find('div', class_='div-product-detail')
internal_code = product_div.find_all('li')[0].get_text() # 예: "008709"
session.post('/Home/DataCart/add', data={
'productCode': internal_code, # 내부 코드
'orderQty': 1
})
# 결과: {"result": 1} 성공!
```
### 2. X-Requested-With 헤더 필요
```python
session.headers.update({
'X-Requested-With': 'XMLHttpRequest' # AJAX 요청임을 명시
})
```
### 3. 세션 쿠키 유지
Playwright로 로그인 → requests 세션에 쿠키 복사:
```python
# Playwright에서 쿠키 획득
cookies = await page.context.cookies()
# requests 세션에 복사
session = requests.Session()
for c in cookies:
session.cookies.set(c['name'], c['value'])
```
### 4. 로그인 세션 만료
- 세션 유효시간: 약 30분
- 해결: 로그인 후 시간 체크, 만료 시 재로그인
```python
if time.time() - self.last_login > 1800: # 30분
self.login()
```
---
## 📊 성능 비교
| 방식 | 첫 요청 | 이후 요청 | 비고 |
|------|---------|----------|------|
| **Playwright** | ~12초 | ~30초 | 브라우저 실행 |
| **API 직접 호출** | **~5초** | **~1초** | requests 사용 |
**30배 속도 향상!**
---
## 🛠️ 구현 코드
### GeoyoungSession 클래스 (geoyoung_api.py)
```python
class GeoyoungSession:
"""지오영 세션 관리 (싱글톤, 세션 재사용)"""
BASE_URL = "https://gwn.geoweb.kr"
def login(self) -> bool:
"""Playwright로 로그인 → 쿠키 획득"""
# ... Playwright 로그인 ...
cookies = await page.context.cookies()
for c in cookies:
self.session.cookies.set(c['name'], c['value'])
self.logged_in = True
self.last_login = time.time()
def search_stock_with_code(self, keyword: str) -> list:
"""검색 + 내부 코드 추출"""
resp = self.session.post(f"{self.BASE_URL}/Home/PartialSearchProduct",
data={'srchText': keyword})
# HTML 파싱 → internal_code 추출
def add_to_cart(self, product_code: str, quantity: int) -> dict:
"""장바구니 추가"""
resp = self.session.post(f"{self.BASE_URL}/Home/DataCart/add", data={
'productCode': product_code,
'moveCode': '',
'orderQty': quantity
})
return resp.json()
def confirm_order(self, memo: str = '') -> dict:
"""주문 확정"""
resp = self.session.post(f"{self.BASE_URL}/Home/DataOrder",
data={'p_desc': memo})
return {'success': True}
def full_order(self, kd_code: str, quantity: int, ...) -> dict:
"""전체 주문 플로우"""
# 1. 검색 → internal_code
# 2. 장바구니 추가
# 3. 주문 확정
```
---
## 🔧 분석 도구/스크립트
분석에 사용한 스크립트들 (backend/ 폴더):
| 파일 | 용도 |
|------|------|
| `capture_geoyoung_api.py` | 네트워크 요청 캡처 |
| `analyze_geoyoung.py` | HTML/JS 분석 |
| `download_js.py` | JS 번들 다운로드 |
| `extract_addcart.py` | AddCart 함수 추출 |
| `extract_processcart.py` | ProcessCart 함수 추출 |
| `find_frmsave.py` | 주문 확정 폼 찾기 |
| `test_datacart.py` | 장바구니 API 테스트 |
| `test_dataorder.py` | 전체 플로우 테스트 |
---
## 📝 API 엔드포인트 (Flask)
```
GET /api/geoyoung/stock?kd_code=661700390 # 재고 조회
POST /api/geoyoung/order # 장바구니 추가
POST /api/geoyoung/confirm # 주문 확정
POST /api/geoyoung/full-order # 전체 주문 (추천!)
```
### full-order 요청 예시
```bash
curl -X POST http://localhost:7001/api/geoyoung/full-order \
-H "Content-Type: application/json" \
-d '{
"kd_code": "661700390",
"quantity": 2,
"specification": "30T",
"auto_confirm": true,
"memo": "자동주문"
}'
```
### 응답
```json
{
"success": true,
"message": "콩코르정2.5mg 30T 머크(대웅) 2개 주문 완료",
"product": {
"insurance_code": "661700390",
"internal_code": "008709",
"product_name": "콩코르정2.5mg 30T 머크(대웅)",
"specification": "30T",
"stock": 533
},
"quantity": 2,
"confirmed": true
}
```
---
## 🎯 핵심 교훈
1. **웹사이트 = API 서버**
모든 웹사이트는 내부적으로 API를 사용함. 브라우저 개발자도구로 분석 가능.
2. **JavaScript 번들 분석**
minified JS도 함수명, URL 패턴으로 핵심 로직 파악 가능.
3. **쿠키 = 인증**
대부분의 사이트는 쿠키로 세션 관리. 쿠키만 있으면 requests로 동일 동작.
4. **내부 코드 ≠ 외부 코드**
보험코드, 바코드 등 외부 식별자와 내부 DB 키가 다를 수 있음.
5. **AJAX 헤더**
`X-Requested-With: XMLHttpRequest` 헤더가 필요한 경우 많음.
---
## 🔮 향후 개선
- [ ] 로그인을 requests로 직접 (Playwright 없이)
- [ ] 다중 도매상 지원 (수인, 백제 등)
- [ ] 주문 실패 시 자동 재시도
- [ ] 주문 상태 조회 API
---
## 📚 참고
- 지오영 URL: https://gwn.geoweb.kr
- 관련 파일: `backend/geoyoung_api.py`
- 주문 DB: `backend/db/orders.db`

View File

@@ -0,0 +1,316 @@
# 전문의약품 사용량 조회 + 지오영 주문 시스템
> 작성일: 2026-03-06
> 상태: 1단계 완료 (재고 조회), 2단계 진행 예정 (자동 주문)
---
## 📋 개요
약국의 전문의약품(처방전 조제) 사용량을 기간별로 조회하고, 지오영 도매상에서 재고를 확인하여 주문까지 연결하는 시스템.
### 핵심 기능
1. **사용량 조회**: 기간별 전문의약품 사용량 집계
2. **현재고 표시**: PIT3000 재고 데이터 연동
3. **지오영 재고 조회**: 도매상 재고 실시간 확인
4. **규격별 표시**: 30T, 100T, 300T 등 다양한 규격
5. **주문 장바구니**: 선택 품목 장바구니 담기
---
## 🗂️ 파일 구조
```
pharmacy-pos-qr-system/backend/
├── app.py # Flask 메인 (Blueprint 등록)
├── geoyoung_api.py # 지오영 API 모듈 ⭐ NEW
└── templates/
├── admin_rx_usage.html # 전문의약품 사용량 페이지 ⭐ NEW
└── admin_usage.html # OTC 사용량 페이지 ⭐ NEW
```
---
## 🔗 API 엔드포인트
### 1. 전문의약품 사용량 조회
```
GET /api/rx-usage?start_date=2026-03-01&end_date=2026-03-06&sort=qty_desc
```
**파라미터:**
| 파라미터 | 설명 | 예시 |
|---------|------|------|
| start_date | 시작일 (YYYY-MM-DD) | 2026-03-01 |
| end_date | 종료일 (YYYY-MM-DD) | 2026-03-06 |
| search | 검색어 (약품명, 코드) | 레바미피드 |
| sort | 정렬 (qty_desc, qty_asc, name_asc, amount_desc, rx_desc) | qty_desc |
**응답:**
```json
{
"success": true,
"items": [
{
"drug_code": "670400830",
"product_name": "휴니즈레바미피드정_(0.1g/1정)",
"supplier": "(주)휴온스메디텍",
"total_qty": 15,
"total_dose": 980,
"total_amount": 12500,
"prescription_count": 45,
"current_stock": 3809,
"barcode": "",
"thumbnail": null
}
],
"stats": {
"period_days": 6,
"product_count": 312,
"total_qty": 1500,
"total_dose": 15042,
"total_amount": 321837881
}
}
```
### 2. 지오영 재고 조회 (보험코드)
```
GET /api/geoyoung/stock?kd_code=670400830
```
**응답:**
```json
{
"success": true,
"keyword": "670400830",
"count": 2,
"items": [
{
"insurance_code": "670400830",
"manufacturer": "휴온스메디텍",
"product_name": "레바미피드정 300T 휴온스메디케어(구.휴니즈)",
"specification": "300T",
"stock": 0
},
{
"insurance_code": "670400830",
"manufacturer": "휴온스메디텍",
"product_name": "레바미피드정 30T 휴온스메디케어(구.휴니즈)",
"specification": "30T",
"stock": 0
}
]
}
```
### 3. 지오영 재고 조회 (제품명 → 성분 추출)
```
GET /api/geoyoung/stock-by-name?product_name=휴니즈레바미피드정_(0.1g/1정)
```
성분명 "레바미피드"를 추출하여 검색 → 여러 제약사 제품 반환
### 4. 지오영 세션 상태
```
GET /api/geoyoung/session-status
```
---
## 🗄️ 데이터베이스 구조
### MSSQL - PM_PRES (처방전)
**PS_sub_pharm** - 처방 상세
| 컬럼 | 설명 |
|------|------|
| PreSerial | 처방전 일련번호 |
| Indate | 조제일 (YYYYMMDD) |
| DrugCode | 약품코드 |
| QUAN | 수량 |
| Days | 투약일수 |
| DRUPRICE | 약가 |
### MSSQL - PM_DRUG (약품)
**CD_GOODS** - 약품 마스터
| 컬럼 | 설명 |
|------|------|
| DrugCode | 약품코드 (PK) |
| GoodsName | 약품명 |
| SplName | 제조사명 |
| BARCODE | 바코드 |
**IM_total** - 현재고 ⭐ 중요
| 컬럼 | 설명 |
|------|------|
| DrugCode | 약품코드 |
| **IM_QT_sale_debit** | **현재고 수량** |
### 현재고 조회 쿼리
```sql
SELECT
P.DrugCode,
G.GoodsName,
ISNULL(IT.IM_QT_sale_debit, 0) as current_stock
FROM PS_sub_pharm P
LEFT JOIN PM_DRUG.dbo.CD_GOODS G ON P.DrugCode = G.DrugCode
LEFT JOIN PM_DRUG.dbo.IM_total IT ON P.DrugCode = IT.DrugCode
```
---
## 🏭 지오영 API 연동
### 아키텍처
```
[브라우저] → [Flask API] → [GeoyoungSession] → [지오영 웹]
[Playwright 로그인] (최초 1회)
[requests 검색] (이후 빠름)
```
### 세션 관리 (geoyoung_api.py)
```python
class GeoyoungSession:
"""싱글톤 패턴, 세션 30분 유지"""
def login(self):
# Playwright로 로그인 → 쿠키 획득
# requests 세션에 쿠키 복사
def search_stock(self, keyword):
# requests로 빠른 검색
# POST /Home/PartialSearchProduct
```
### 성능
| 요청 | 소요시간 | 비고 |
|------|----------|------|
| 첫 요청 (로그인) | ~12초 | Playwright 브라우저 |
| 이후 요청 | **~2.5초** | requests 재사용 |
| 세션 유효기간 | 30분 | 자동 재로그인 |
### 지오영 로그인 정보
```
URL: https://gwn.geoweb.kr
ID: 7390
PW: trajet6640
```
---
## 💻 UI 사용법
### 페이지 접속
```
http://localhost:7001/admin/rx-usage
```
### 기능
1. **날짜 선택**: 시작일/종료일 지정
2. **검색**: 약품명, 코드로 필터
3. **정렬**: 투약량순, 처방건수순, 금액순
4. **지오영 조회**: 행 **더블클릭** → 모달
5. **장바구니**: 체크 후 "장바구니 추가"
6. **주문서**: "주문서 생성" → 클립보드 복사
### 색상 의미 (현재고)
- 🟢 초록: 재고 충분 (현재고 > 사용량)
- 🟡 노랑: 재고 부족 (현재고 < 사용량)
- 🔴 빨강: 재고 없음 (0)
---
## 🚀 향후 개발 계획
### 2단계: 자동 주문
- [ ] 지오영 장바구니 담기 API
- [ ] 주문 확정 API (dry_run 모드)
- [ ] 주문 내역 SQLite 저장
### 3단계: 다중 도매상
- [ ] 수인 API 연동
- [ ] 도매상 선택 UI
- [ ] 재고 비교 (A사 vs B사)
### 4단계: 스마트 주문
- [ ] 사용량 기반 최적 규격 추천
- 예: 220개 필요 → "30T x 8개" vs "300T x 1개"
- [ ] 분할 주문 (오전/오후)
- [ ] 주문 누적 관리
### 5단계: 주문 DB
```sql
-- SQLite: orders.db
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
order_date TEXT,
wholesaler TEXT, -- 'geoyoung', 'sooin'
drug_code TEXT,
product_name TEXT,
specification TEXT, -- '30T', '300T'
quantity INTEGER,
status TEXT, -- 'pending', 'ordered', 'delivered'
created_at TEXT
);
```
---
## 🔧 트러블슈팅
### 문제: 지오영 로그인 실패
**원인**: requests만으로는 로그인 불가 (JavaScript 필요)
**해결**: Playwright 하이브리드 방식 (로그인만 Playwright)
### 문제: 검색 결과 0개
**원인**: 보험코드가 아닌 내부 코드로 검색
**해결**: 보험코드(KD코드) 사용, 또는 성분명으로 재검색
### 문제: 현재고가 0으로 표시
**원인**: IM_inventory 테이블이 비어있음
**해결**: `IM_total.IM_QT_sale_debit` 컬럼 사용
### 문제: Flask 서버 시작 안됨
**원인**: stdout 인코딩 문제 (Start-Process 사용 시)
**해결**: geoyoung_api.py에서 stdout 재설정 코드 제거
---
## 📝 관련 파일 참조
### 지오영 크롤러 원본
```
c:\Users\청춘약국\source\person-lookup-web-local\crawler\
├── gangwon_geoyoung_api.py # API 클라이언트
├── gangwon_geoyoung_order.py # 주문 자동화 (order_by_kd_code)
└── gangwon_geoyoung_crawler.py # 데이터 크롤링
```
### 주문 함수 사용 예시
```python
from gangwon_geoyoung_order import order_by_kd_code
# 테스트 (실제 주문 안함)
result = await order_by_kd_code("670400830", quantity=10, dry_run=True)
# 실제 주문
result = await order_by_kd_code("670400830", quantity=10, dry_run=False)
```
---
## ✅ 체크리스트
- [x] 전문의약품 사용량 조회 API
- [x] 현재고 표시 (IM_total)
- [x] 지오영 재고 조회 API
- [x] 지오영 세션 관리 (속도 개선)
- [x] UI 모달 (더블클릭)
- [x] 장바구니 기능
- [ ] 지오영 실제 주문 연동
- [ ] 주문 내역 DB 저장
- [ ] 다중 도매상 지원

View File

@@ -0,0 +1,189 @@
# 도매상 API 통합 가이드
> 작성일: 2026-03-06
> 버전: 1.0
## 📦 패키지 구조
```
pharmacy-wholesale-api/ # 별도 리포지토리
├── wholesale/
│ ├── __init__.py # SooinSession, GeoYoungSession 노출
│ ├── base.py # WholesaleSession 공통 인터페이스
│ ├── sooin.py # 수인약품 API
│ └── geoyoung.py # 지오영 API
└── docs/
└── SOOIN.md # 수인약품 상세 문서
pharmacy-pos-qr-system/backend/ # 기존 프로젝트
├── wholesale_path.py # 패키지 경로 설정
├── sooin_api.py # Flask Blueprint (wholesale 사용)
└── geoyoung_api.py # Flask Blueprint (wholesale 사용)
```
---
## 🔌 도매상별 API 특성
| 항목 | 지오영 | 수인약품 |
|------|--------|----------|
| 웹사이트 | gwn.geoweb.kr | sooinpharm.co.kr |
| 인증 방식 | Playwright → requests | Playwright → requests |
| 세션 유효시간 | 30분 | 30분 |
| 검색 코드 | 보험코드 (KD) | KD코드 + 내부코드 (pc) |
| 장바구니 추가 | productCode 필요 | internal_code (pc) 필요 |
| **개별 삭제** | ❌ 없음 | ✅ 체크박스 soft delete |
| 장바구니 조회 | PartialProductCart | Bag.asp |
---
## 🔑 핵심 발견: 코드 체계
### 지오영
```
보험코드 (KD코드) → 검색 → productCode (내부) → 장바구니 추가
```
### 수인약품
```
KD코드 → 검색 → internal_code (pc) → 장바구니 추가
PhysicInfo.asp?pc=32495 에서 추출
```
**⚠️ 중요:** `internal_code`가 없으면 장바구니 추가 불가!
---
## 🛒 수인약품 개별 취소 (Soft Delete)
### 발견 과정
- `kind=delOne` API 존재하지만 작동 안 함
- 체크박스가 실제 "취소" 역할
- `ControlBag.asp` AJAX 엔드포인트 발견
### API 사용법
```python
from wholesale import SooinSession
session = SooinSession()
session.login()
# 장바구니 조회 (체크 상태 포함)
cart = session.get_cart()
# cart['items'][0]['checked'] = False (활성)
# cart['items'][0]['active'] = True
# 항목 취소 (체크)
session.cancel_item(row_index=0)
# 또는
session.cancel_item(product_code="32495")
# 취소 복원 (체크 해제)
session.restore_item(row_index=0)
```
### 내부 동작
```
POST /Service/Order/ControlBag.asp
Content-Type: application/x-www-form-urlencoded; charset=euc-kr
X-Requested-With: XMLHttpRequest
vc=50911 (거래처코드)
pc=32495 (내부 제품코드)
f=true (true=취소, false=복원)
pg= (제품구분, 빈값)
pdno= (제품번호, 빈값)
tmdt= (기한, 빈값)
```
---
## 📊 SQLite 스키마 연동
### order_context (AI 학습용)
```sql
-- 새로 추가된 필드 (2026-03-06)
wholesaler_id TEXT, -- 'geoyoung' 또는 'sooin'
wholesaler_price INTEGER, -- 도매상 가격
internal_code TEXT, -- 도매상 내부 코드
was_cancelled BOOLEAN, -- 취소 여부 (수인 soft delete)
```
### 도매상별 주문 시 기록할 데이터
```python
order_context = {
'drug_code': 'D12345',
'product_name': '아세탑정',
'wholesaler_id': 'sooin',
'internal_code': '32495', # 수인 내부코드
'ordered_spec': '30T',
'ordered_qty': 2,
'wholesaler_price': 4800,
'available_specs': '["30T", "500T"]',
'spec_stocks': '{"30T": 0, "500T": 0}', # 재고 상황
'selection_reason': 'only_option',
'was_cancelled': False
}
```
---
## 🔄 Flask API 엔드포인트
### 수인약품 (/api/sooin/*)
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | /stock | 재고 검색 |
| GET | /cart | 장바구니 조회 |
| POST | /order | 장바구니 추가 |
| POST | /cart/clear | 장바구니 비우기 |
| POST | /cart/cancel | **항목 취소** (신규) |
| POST | /cart/restore | **항목 복원** (신규) |
| POST | /confirm | 주문 전송 |
### 지오영 (/api/geoyoung/*)
| 메서드 | 경로 | 설명 |
|--------|------|------|
| GET | /stock | 재고 검색 |
| GET | /cart | 장바구니 조회 |
| POST | /order | 장바구니 추가 |
| POST | /cart/clear | 장바구니 비우기 |
| POST | /confirm | 주문 전송 |
---
## 📁 관련 문서
| 문서 | 위치 | 내용 |
|------|------|------|
| AI ERP 자동주문 기획 | `docs/AI_ERP_AUTO_ORDER_SYSTEM.md` | 전체 시스템 설계 |
| 지오영 API 분석 | `docs/GEOYOUNG_API_REVERSE_ENGINEERING.md` | 지오영 리버스 엔지니어링 |
| 수인 API 분석 | `pharmacy-wholesale-api/docs/SOOIN.md` | 수인 리버스 엔지니어링 |
| 사용량 조회 가이드 | `docs/RX_USAGE_GEOYOUNG_GUIDE.md` | 처방 사용량 조회 |
---
## ✅ 체크리스트
### 완료
- [x] 지오영 API 연동
- [x] 수인약품 API 연동
- [x] 개별 취소 기능 (수인)
- [x] Flask Blueprint 통합
- [x] wholesale 패키지 분리
- [x] SQLite 스키마 업데이트
### 진행 예정
- [ ] daily_usage 자동 수집
- [ ] AI 규격 선택 모델
- [ ] AI 도매상 선택 모델
- [ ] 자동 주문 Level 1 (승인 후 실행)
---
*업데이트: 2026-03-06 by 용림 🐉*