pharmacy-pos-qr-system/backend/order_api.py
thug0bin a672c7a2a0 feat(order): 지오영/수인 선택적 주문 + 장바구니 보존 기능
- internal_code DB 저장 → 프론트에서 선택한 제품 그대로 주문
- 기존 장바구니 백업/복구로 사용자 장바구니 보존
- 수인약품 submit_order() 수정 (체크박스 제외 방식)
- 테스트 파일 정리 및 문서 추가
2026-03-06 23:26:44 +09:00

1059 lines
45 KiB
Python

# -*- coding: utf-8 -*-
"""
주문 API 모듈
- 주문 생성/조회
- 지오영 실제 주문 연동
- dry_run 테스트 모드
"""
import sys
import os
import asyncio
import re
from flask import Blueprint, jsonify, request
import logging
logger = logging.getLogger(__name__)
# Blueprint 생성
order_bp = Blueprint('order', __name__, url_prefix='/api/order')
# 지오영 크롤러 경로
CRAWLER_PATH = r'c:\Users\청춘약국\source\person-lookup-web-local\crawler'
if CRAWLER_PATH not in sys.path:
sys.path.insert(0, CRAWLER_PATH)
# 주문 DB
from order_db import (
create_order, get_order, update_order_status,
update_item_result, get_order_history,
save_order_context, get_usage_stats, get_order_pattern,
get_ai_training_data
)
def run_async(coro):
"""동기 컨텍스트에서 비동기 함수 실행"""
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
return loop.run_until_complete(coro)
def parse_specification(spec: str) -> int:
"""규격에서 숫자 추출 (30T -> 30)"""
if not spec:
return 1
match = re.search(r'(\d+)', spec)
return int(match.group(1)) if match else 1
# ─────────────────────────────────────────────
# API 엔드포인트
# ─────────────────────────────────────────────
@order_bp.route('/create', methods=['POST'])
def api_create_order():
"""
주문 생성 (draft 상태)
POST /api/order/create
{
"wholesaler_id": "geoyoung",
"items": [
{
"drug_code": "670400830",
"kd_code": "670400830",
"product_name": "레바미피드정 30T",
"specification": "30T",
"order_qty": 10,
"usage_qty": 280,
"current_stock": 50
}
],
"reference_period": "2026-03-01~2026-03-06",
"note": "오전 주문"
}
"""
data = request.get_json()
if not data:
return jsonify({'success': False, 'error': 'No data'}), 400
wholesaler_id = data.get('wholesaler_id', 'geoyoung')
items = data.get('items', [])
if not items:
return jsonify({'success': False, 'error': 'No items'}), 400
# unit_qty 계산
for item in items:
if 'unit_qty' not in item:
item['unit_qty'] = parse_specification(item.get('specification'))
result = create_order(
wholesaler_id=wholesaler_id,
items=items,
order_type=data.get('order_type', 'manual'),
order_session=data.get('order_session'),
reference_period=data.get('reference_period'),
note=data.get('note')
)
return jsonify(result)
@order_bp.route('/submit', methods=['POST'])
def api_submit_order():
"""
주문 제출 (실제 도매상 주문)
POST /api/order/submit
{
"order_id": 1,
"dry_run": true
}
dry_run=true: 시뮬레이션 (실제 주문 X)
dry_run=false: 실제 주문
"""
data = request.get_json()
order_id = data.get('order_id')
dry_run = data.get('dry_run', True) # 기본은 테스트 모드
if not order_id:
return jsonify({'success': False, 'error': 'order_id required'}), 400
# 주문 조회
order = get_order(order_id)
if not order:
return jsonify({'success': False, 'error': 'Order not found'}), 404
if order['status'] not in ('draft', 'pending', 'failed'):
return jsonify({
'success': False,
'error': f"Cannot submit order with status: {order['status']}"
}), 400
wholesaler_id = order['wholesaler_id']
# 도매상별 주문 처리
if wholesaler_id == 'geoyoung':
result = submit_geoyoung_order(order, dry_run)
else:
result = {
'success': False,
'error': f'Wholesaler {wholesaler_id} not supported yet'
}
return jsonify(result)
def submit_geoyoung_order(order: dict, dry_run: bool, cart_only: bool = True) -> dict:
"""
지오영 주문 제출
Args:
order: 주문 정보
dry_run: True=시뮬레이션만, False=실제 주문
cart_only: True=장바구니만, False=주문 확정까지
"""
order_id = order['id']
items = order['items']
# 상태 업데이트
update_order_status(order_id, 'pending',
f'주문 제출 시작 (dry_run={dry_run}, cart_only={cart_only})')
results = []
success_count = 0
failed_count = 0
try:
if dry_run:
# ─────────────────────────────────────────
# DRY RUN: 시뮬레이션
# ─────────────────────────────────────────
for item in items:
# 재고 확인만 (실제 주문 X)
from geoyoung_api import search_geoyoung_stock
kd_code = item.get('kd_code') or item.get('drug_code')
stock_result = search_geoyoung_stock(kd_code)
# 규격 매칭 (재고 있는 것 우선!)
spec = item.get('specification', '')
matched = None
matched_with_stock = None
matched_any = None
if stock_result.get('success'):
for geo_item in stock_result.get('items', []):
if spec in geo_item.get('specification', ''):
# 첫 번째 규격 매칭 저장
if matched_any is None:
matched_any = geo_item
# 재고 있는 제품 우선
if geo_item.get('stock', 0) > 0:
matched_with_stock = geo_item
break
# 재고 있는 것 우선, 없으면 첫 번째 매칭
matched = matched_with_stock or matched_any
# 모든 규격과 재고 수집 (AI 학습용)
available_specs = []
spec_stocks = {}
if stock_result.get('success'):
for geo_item in stock_result.get('items', []):
s = geo_item.get('specification', '')
available_specs.append(s)
spec_stocks[s] = geo_item.get('stock', 0)
if matched:
if matched['stock'] >= item['order_qty']:
# 주문 가능
status = 'success'
result_code = 'OK'
result_message = f"[DRY RUN] 주문 가능: 재고 {matched['stock']}"
success_count += 1
selection_reason = 'stock_available'
else:
# 재고 부족
status = 'failed'
selection_reason = 'low_stock'
result_code = 'LOW_STOCK'
result_message = f"[DRY RUN] 재고 부족: {matched['stock']}개 (요청: {item['order_qty']})"
failed_count += 1
else:
# 제품 없음
status = 'failed'
result_code = 'NOT_FOUND'
result_message = f"[DRY RUN] 지오영에서 규격 {spec} 미발견"
failed_count += 1
selection_reason = 'not_found'
update_item_result(item['id'], status, result_code, result_message)
# ─────────────────────────────────────────
# AI 학습용 컨텍스트 저장
# ─────────────────────────────────────────
save_order_context(item['id'], {
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'stock_at_order': item.get('current_stock', 0),
'usage_1d': item.get('usage_qty', 0) // 7 if item.get('usage_qty') else 0, # 추정
'usage_7d': item.get('usage_qty', 0), # 조회 기간 사용량
'usage_30d': (item.get('usage_qty', 0) * 30) // 7 if item.get('usage_qty') else 0, # 추정
'ordered_spec': spec,
'ordered_qty': item['order_qty'],
'available_specs': available_specs,
'spec_stocks': spec_stocks,
'selection_reason': selection_reason if 'selection_reason' in dir() else 'unknown'
})
results.append({
'item_id': item['id'],
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'specification': spec,
'order_qty': item['order_qty'],
'status': status,
'result_code': result_code,
'result_message': result_message,
'available_specs': available_specs,
'spec_stocks': spec_stocks
})
# 주문 상태 업데이트
if failed_count == 0:
update_order_status(order_id, 'completed',
f'[DRY RUN] 시뮬레이션 완료: {success_count}개 성공')
elif success_count == 0:
update_order_status(order_id, 'failed',
f'[DRY RUN] 시뮬레이션 완료: {failed_count}개 실패')
else:
update_order_status(order_id, 'partial',
f'[DRY RUN] 부분 성공: {success_count}개 성공, {failed_count}개 실패')
else:
# ─────────────────────────────────────────
# 실제 주문 (선별 주문 - 기존 장바구니 보존)
# ─────────────────────────────────────────
from geoyoung_api import get_geo_session
geo_session = get_geo_session()
# 1단계: 모든 품목을 장바구니에 담기
for item in items:
kd_code = item.get('kd_code') or item.get('drug_code')
order_qty = item['order_qty']
spec = item.get('specification', '')
item_internal_code = item.get('internal_code') # 프론트에서 이미 선택한 품목
result = {}
# 🔍 디버그 로그
logger.info(f"[GEO DEBUG] item keys: {list(item.keys())}")
logger.info(f"[GEO DEBUG] kd_code={kd_code}, internal_code={item_internal_code}, qty={order_qty}, spec={spec}")
logger.info(f"[GEO DEBUG] full item: {item}")
try:
if item_internal_code:
# internal_code가 있으면 검색 없이 바로 장바구니 추가!
logger.info(f"[GEO DEBUG] Using internal_code directly: {item_internal_code}")
result = geo_session.add_to_cart(item_internal_code, order_qty)
logger.info(f"[GEO DEBUG] add_to_cart result: {result}")
if result.get('success'):
result['product'] = {'internal_code': item_internal_code, 'name': item.get('product_name', '')}
else:
# internal_code 없으면 검색 후 장바구니 추가
logger.info(f"[GEO DEBUG] No internal_code, using full_order with kd_code={kd_code}")
result = geo_session.full_order(
kd_code=kd_code,
quantity=order_qty,
specification=spec if spec else None,
check_stock=True,
auto_confirm=False,
memo=f"자동주문 - {item.get('product_name', '')}"
)
logger.info(f"[GEO DEBUG] full_order result: {result}")
if result.get('success'):
status = 'success'
result_code = 'CART_ADDED'
result_message = '장바구니 추가 완료'
success_count += 1
else:
status = 'failed'
result_code = result.get('error', 'UNKNOWN')
result_message = result.get('message', '장바구니 추가 실패')
failed_count += 1
except Exception as e:
status = 'failed'
result_code = 'ERROR'
result_message = str(e)
failed_count += 1
update_item_result(item['id'], status, result_code, result_message)
# full_order 결과에서 internal_code 가져오기 (지오영은 internal_code 사용!)
internal_code = result.get('product', {}).get('internal_code') if result.get('success') else None
# AI 학습용 컨텍스트 저장
save_order_context(item['id'], {
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'stock_at_order': item.get('current_stock', 0),
'usage_7d': item.get('usage_qty', 0),
'ordered_spec': spec,
'ordered_qty': order_qty,
'selection_reason': 'user_order',
'wholesaler_id': 'geoyoung',
'internal_code': internal_code
})
results.append({
'item_id': item['id'],
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'specification': spec,
'order_qty': order_qty,
'status': status,
'result_code': result_code,
'result_message': result_message,
'internal_code': internal_code # 선별 주문용
})
# 2단계: cart_only=False면 선별 주문 (기존 품목 보존)
if not cart_only and success_count > 0:
try:
# 이번에 담은 품목의 internal_code만 수집
ordered_codes = [r['internal_code'] for r in results
if r['status'] == 'success' and r.get('internal_code')]
# 🔧 디버그: 선별 주문 전 상세 로그
logger.info(f"[GEO DEBUG] 선별 주문 시작")
logger.info(f"[GEO DEBUG] ordered_codes: {ordered_codes}")
logger.info(f"[GEO DEBUG] results: {[(r.get('product_name', '')[:20], r.get('internal_code')) for r in results if r['status'] == 'success']}")
if ordered_codes:
# 선별 주문: 기존 품목은 건드리지 않고, 이번에 담은 것만 주문
confirm_result = geo_session.submit_order_selective(ordered_codes)
if confirm_result.get('success'):
restored_info = f", 기존 {confirm_result.get('restored_count', 0)}개 복원" if confirm_result.get('restored_count', 0) > 0 else ""
update_order_status(order_id, 'submitted',
f'지오영 주문 확정 완료: {success_count}{restored_info}')
# 결과 메시지 업데이트
for r in results:
if r['status'] == 'success':
r['result_code'] = 'OK'
r['result_message'] = '주문 확정 완료'
else:
update_order_status(order_id, 'partial',
f'지오영 장바구니 담김, 확정 실패: {confirm_result.get("error", "알 수 없는 오류")}')
else:
update_order_status(order_id, 'partial',
f'지오영 장바구니 담김, internal_code 없음')
except Exception as e:
logger.error(f"지오영 주문 확정 오류: {e}")
update_order_status(order_id, 'partial',
f'지오영 장바구니 담김, 확정 중 오류: {str(e)}')
elif success_count > 0:
# cart_only=True: 장바구니만
if failed_count == 0:
update_order_status(order_id, 'pending',
f'지오영 장바구니 추가 완료: {success_count}개 (사이트에서 확정 필요)')
else:
update_order_status(order_id, 'partial',
f'부분 성공: {success_count}개 장바구니, {failed_count}개 실패')
else:
update_order_status(order_id, 'failed',
f'지오영 주문 실패: {failed_count}')
return {
'success': True,
'dry_run': dry_run,
'cart_only': cart_only,
'order_id': order_id,
'order_no': order['order_no'],
'wholesaler': 'geoyoung',
'total_items': len(items),
'success_count': success_count,
'failed_count': failed_count,
'results': results,
'note': '지오영 장바구니에 담김. 지오영 사이트에서 최종 확정 필요.' if cart_only else None
}
except Exception as e:
logger.error(f"지오영 주문 오류: {e}")
update_order_status(order_id, 'failed', str(e))
return {
'success': False,
'order_id': order_id,
'error': str(e)
}
@order_bp.route('/<int:order_id>', methods=['GET'])
def api_get_order(order_id):
"""주문 상세 조회"""
order = get_order(order_id)
if not order:
return jsonify({'success': False, 'error': 'Order not found'}), 404
return jsonify({'success': True, 'order': order})
@order_bp.route('/history', methods=['GET'])
def api_order_history():
"""
주문 이력 조회
GET /api/order/history?wholesaler_id=geoyoung&start_date=2026-03-01&limit=20
"""
orders = get_order_history(
wholesaler_id=request.args.get('wholesaler_id'),
start_date=request.args.get('start_date'),
end_date=request.args.get('end_date'),
status=request.args.get('status'),
limit=int(request.args.get('limit', 50))
)
return jsonify({
'success': True,
'count': len(orders),
'orders': orders
})
@order_bp.route('/quick-submit', methods=['POST'])
def api_quick_submit():
"""
빠른 주문 (생성 + 제출 한번에)
POST /api/order/quick-submit
{
"wholesaler_id": "geoyoung" | "sooin" | "baekje",
"items": [...],
"dry_run": true,
"cart_only": true // true=장바구니만, false=실제 주문까지
}
"""
data = request.get_json()
if not data or not data.get('items'):
return jsonify({'success': False, 'error': 'No items'}), 400
# 1. 주문 생성
create_result = create_order(
wholesaler_id=data.get('wholesaler_id', 'geoyoung'),
items=data['items'],
order_type='manual',
reference_period=data.get('reference_period'),
note=data.get('note')
)
if not create_result.get('success'):
return jsonify(create_result), 400
order_id = create_result['order_id']
# 2. 주문 조회
order = get_order(order_id)
# 3. 주문 제출
dry_run = data.get('dry_run', True)
cart_only = data.get('cart_only', True) # 기본값: 장바구니만
if order['wholesaler_id'] == 'geoyoung':
submit_result = submit_geoyoung_order(order, dry_run, cart_only=cart_only)
elif order['wholesaler_id'] == 'sooin':
submit_result = submit_sooin_order(order, dry_run, cart_only=cart_only)
elif order['wholesaler_id'] == 'baekje':
submit_result = submit_baekje_order(order, dry_run, cart_only=cart_only)
else:
submit_result = {'success': False, 'error': f"Wholesaler {order['wholesaler_id']} not supported"}
submit_result['order_no'] = create_result['order_no']
return jsonify(submit_result)
def submit_sooin_order(order: dict, dry_run: bool, cart_only: bool = True) -> dict:
"""
수인약품 주문 제출
Args:
order: 주문 정보
dry_run: True=시뮬레이션만, False=실제 주문
cart_only: True=장바구니만, False=주문 확정까지
"""
order_id = order['id']
items = order['items']
# 상태 업데이트
update_order_status(order_id, 'pending',
f'수인 주문 시작 (dry_run={dry_run}, cart_only={cart_only})')
results = []
success_count = 0
failed_count = 0
try:
from sooin_api import get_sooin_session
sooin_session = get_sooin_session()
if dry_run:
# ─────────────────────────────────────────
# DRY RUN: 재고 확인만
# ─────────────────────────────────────────
for item in items:
kd_code = item.get('kd_code') or item.get('drug_code')
spec = item.get('specification', '')
# 재고 검색
search_result = sooin_session.search_products(kd_code)
matched = None
available_specs = []
spec_stocks = {}
if search_result.get('success'):
for sooin_item in search_result.get('items', []):
s = sooin_item.get('spec', '')
available_specs.append(s)
spec_stocks[s] = sooin_item.get('stock', 0)
# 규격 매칭
if spec in s or s in spec:
if matched is None or sooin_item.get('stock', 0) > matched.get('stock', 0):
matched = sooin_item
if matched:
stock = matched.get('stock', 0)
if stock >= item['order_qty']:
status = 'success'
result_code = 'OK'
result_message = f"[DRY RUN] 주문 가능: 재고 {stock}, 단가 {matched.get('price', 0):,}"
success_count += 1
selection_reason = 'stock_available'
elif stock > 0:
status = 'failed'
result_code = 'LOW_STOCK'
result_message = f"[DRY RUN] 재고 부족: {stock}개 (요청: {item['order_qty']})"
failed_count += 1
selection_reason = 'low_stock'
else:
status = 'failed'
result_code = 'OUT_OF_STOCK'
result_message = f"[DRY RUN] 재고 없음"
failed_count += 1
selection_reason = 'out_of_stock'
else:
status = 'failed'
result_code = 'NOT_FOUND'
result_message = f"[DRY RUN] 수인에서 규격 {spec} 미발견"
failed_count += 1
selection_reason = 'not_found'
update_item_result(item['id'], status, result_code, result_message)
# AI 학습용 컨텍스트 저장
save_order_context(item['id'], {
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'stock_at_order': item.get('current_stock', 0),
'usage_7d': item.get('usage_qty', 0),
'ordered_spec': spec,
'ordered_qty': item['order_qty'],
'available_specs': available_specs,
'spec_stocks': spec_stocks,
'selection_reason': selection_reason,
'wholesaler_id': 'sooin'
})
results.append({
'item_id': item['id'],
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'specification': spec,
'order_qty': item['order_qty'],
'status': status,
'result_code': result_code,
'result_message': result_message,
'available_specs': available_specs,
'spec_stocks': spec_stocks,
'price': matched.get('price') if matched else None
})
# 상태 업데이트
if failed_count == 0:
update_order_status(order_id, 'completed',
f'[DRY RUN] 수인 시뮬레이션 완료: {success_count}개 성공')
elif success_count == 0:
update_order_status(order_id, 'failed',
f'[DRY RUN] 수인 시뮬레이션 완료: {failed_count}개 실패')
else:
update_order_status(order_id, 'partial',
f'[DRY RUN] 수인 부분 성공: {success_count}개 성공, {failed_count}개 실패')
else:
# ─────────────────────────────────────────
# 실제 주문
# ─────────────────────────────────────────
for item in items:
kd_code = item.get('kd_code') or item.get('drug_code')
order_qty = item['order_qty']
spec = item.get('specification', '')
try:
# quick_order 사용 (검색 → 가격/재고 정보 포함하여 장바구니 추가)
cart_result = sooin_session.quick_order(
kd_code=kd_code,
quantity=order_qty,
spec=spec if spec else None,
check_stock=False # 재고 체크는 이미 테스트에서 했으므로 스킵
)
if cart_result.get('success'):
status = 'success'
result_code = 'CART_ADDED'
result_message = f"장바구니 추가 완료 (확정 필요)"
success_count += 1
else:
status = 'failed'
result_code = cart_result.get('error', 'CART_FAILED')
result_message = cart_result.get('message', '장바구니 추가 실패')
failed_count += 1
except Exception as e:
status = 'failed'
result_code = 'ERROR'
result_message = str(e)
failed_count += 1
update_item_result(item['id'], status, result_code, result_message)
# quick_order 결과에서 internal_code 가져오기
internal_code = cart_result.get('product', {}).get('internal_code') if cart_result.get('success') else None
save_order_context(item['id'], {
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'stock_at_order': item.get('current_stock', 0),
'usage_7d': item.get('usage_qty', 0),
'ordered_spec': spec,
'ordered_qty': order_qty,
'selection_reason': 'user_order',
'wholesaler_id': 'sooin',
'internal_code': internal_code
})
results.append({
'item_id': item['id'],
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'specification': spec,
'order_qty': order_qty,
'status': status,
'result_code': result_code,
'result_message': result_message,
'internal_code': internal_code # 선별 주문용
})
# cart_only=False면 주문 확정까지 진행 (선별 주문!)
if not cart_only and success_count > 0:
try:
# 이번에 담은 품목의 internal_code만 수집
ordered_codes = [r['internal_code'] for r in results
if r['status'] == 'success' and r.get('internal_code')]
if ordered_codes:
# 선별 주문: 기존 품목은 건드리지 않고, 이번에 담은 것만 주문
confirm_result = sooin_session.submit_order_selective(ordered_codes)
if confirm_result.get('success'):
restored_info = f", 기존 {confirm_result.get('restored_count', 0)}개 복원" if confirm_result.get('restored_count', 0) > 0 else ""
update_order_status(order_id, 'submitted',
f'수인 주문 확정 완료: {success_count}{restored_info}')
# 결과 메시지 업데이트
for r in results:
if r['status'] == 'success':
r['result_code'] = 'OK'
r['result_message'] = '주문 확정 완료'
else:
update_order_status(order_id, 'partial',
f'수인 장바구니 담김, 확정 실패: {confirm_result.get("error", "알 수 없는 오류")}')
else:
update_order_status(order_id, 'partial',
f'수인 장바구니 담김, internal_code 없음')
except Exception as e:
logger.error(f"수인 주문 확정 오류: {e}")
update_order_status(order_id, 'partial',
f'수인 장바구니 담김, 확정 중 오류: {str(e)}')
elif success_count > 0:
update_order_status(order_id, 'pending',
f'수인 장바구니 추가 완료: {success_count}개 (확정 필요)')
else:
update_order_status(order_id, 'failed',
f'수인 주문 실패: {failed_count}')
# 응답 생성
if cart_only:
note = '수인약품 장바구니에 담김. 사이트에서 최종 확정 필요.'
else:
note = None
return {
'success': True,
'dry_run': dry_run,
'cart_only': cart_only,
'order_id': order_id,
'order_no': order['order_no'],
'wholesaler': 'sooin',
'total_items': len(items),
'success_count': success_count,
'failed_count': failed_count,
'results': results,
'note': note if not dry_run else None
}
except Exception as e:
logger.error(f"수인 주문 오류: {e}")
update_order_status(order_id, 'failed', str(e))
return {
'success': False,
'order_id': order_id,
'error': str(e)
}
def submit_baekje_order(order: dict, dry_run: bool, cart_only: bool = True) -> dict:
"""
백제약품 주문 제출
Args:
order: 주문 정보
dry_run: True=시뮬레이션만, False=실제 주문
cart_only: True=장바구니만, False=주문 확정까지
"""
order_id = order['id']
items = order['items']
# 상태 업데이트
update_order_status(order_id, 'pending',
f'백제약품 주문 시작 (dry_run={dry_run}, cart_only={cart_only})')
results = []
success_count = 0
failed_count = 0
try:
from baekje_api import get_baekje_session
baekje_session = get_baekje_session()
if dry_run:
# ─────────────────────────────────────────
# DRY RUN: 재고 확인만
# ─────────────────────────────────────────
for item in items:
kd_code = item.get('kd_code') or item.get('drug_code')
spec = item.get('specification', '')
# 재고 검색
search_result = baekje_session.search_products(kd_code)
matched = None
available_specs = []
spec_stocks = {}
if search_result.get('success'):
for baekje_item in search_result.get('items', []):
s = baekje_item.get('spec', '')
available_specs.append(s)
spec_stocks[s] = baekje_item.get('stock', 0)
# 규격 매칭
if spec in s or s in spec:
if matched is None or baekje_item.get('stock', 0) > matched.get('stock', 0):
matched = baekje_item
if matched:
stock = matched.get('stock', 0)
if stock >= item['order_qty']:
status = 'success'
result_code = 'OK'
result_message = f"[DRY RUN] 주문 가능: 재고 {stock}, 단가 {matched.get('price', 0):,}"
success_count += 1
selection_reason = 'stock_available'
elif stock > 0:
status = 'failed'
result_code = 'LOW_STOCK'
result_message = f"[DRY RUN] 재고 부족: {stock}개 (요청: {item['order_qty']})"
failed_count += 1
selection_reason = 'low_stock'
else:
status = 'failed'
result_code = 'OUT_OF_STOCK'
result_message = f"[DRY RUN] 재고 없음"
failed_count += 1
selection_reason = 'out_of_stock'
else:
status = 'failed'
result_code = 'NOT_FOUND'
result_message = f"[DRY RUN] 백제에서 규격 {spec} 미발견"
failed_count += 1
selection_reason = 'not_found'
update_item_result(item['id'], status, result_code, result_message)
# AI 학습용 컨텍스트 저장
save_order_context(item['id'], {
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'stock_at_order': item.get('current_stock', 0),
'usage_7d': item.get('usage_qty', 0),
'ordered_spec': spec,
'ordered_qty': item['order_qty'],
'available_specs': available_specs,
'spec_stocks': spec_stocks,
'selection_reason': selection_reason,
'wholesaler_id': 'baekje'
})
results.append({
'item_id': item['id'],
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'specification': spec,
'order_qty': item['order_qty'],
'status': status,
'result_code': result_code,
'result_message': result_message,
'available_specs': available_specs,
'spec_stocks': spec_stocks,
'price': matched.get('price') if matched else None
})
# 상태 업데이트
if failed_count == 0:
update_order_status(order_id, 'completed',
f'[DRY RUN] 백제 시뮬레이션 완료: {success_count}개 성공')
elif success_count == 0:
update_order_status(order_id, 'failed',
f'[DRY RUN] 백제 시뮬레이션 완료: {failed_count}개 실패')
else:
update_order_status(order_id, 'partial',
f'[DRY RUN] 백제 부분 성공: {success_count}개 성공, {failed_count}개 실패')
else:
# ─────────────────────────────────────────
# 실제 주문 (장바구니 추가)
# ─────────────────────────────────────────
for item in items:
kd_code = item.get('kd_code') or item.get('drug_code')
order_qty = item['order_qty']
spec = item.get('specification', '')
try:
# 장바구니 추가
cart_result = baekje_session.add_to_cart(kd_code, order_qty)
if cart_result.get('success'):
status = 'success'
result_code = 'CART_ADDED'
result_message = f"장바구니 추가 완료 (백제몰에서 확정 필요)"
success_count += 1
else:
status = 'failed'
result_code = cart_result.get('error', 'CART_FAILED')
result_message = cart_result.get('message', '장바구니 추가 실패')
failed_count += 1
except Exception as e:
status = 'failed'
result_code = 'ERROR'
result_message = str(e)
failed_count += 1
update_item_result(item['id'], status, result_code, result_message)
save_order_context(item['id'], {
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'stock_at_order': item.get('current_stock', 0),
'usage_7d': item.get('usage_qty', 0),
'ordered_spec': spec,
'ordered_qty': order_qty,
'selection_reason': 'user_order',
'wholesaler_id': 'baekje'
})
results.append({
'item_id': item['id'],
'drug_code': item['drug_code'],
'product_name': item['product_name'],
'specification': spec,
'order_qty': order_qty,
'status': status,
'result_code': result_code,
'result_message': result_message
})
# cart_only=False면 주문 확정까지 진행
if not cart_only and success_count > 0:
try:
confirm_result = baekje_session.submit_order()
if confirm_result.get('success'):
update_order_status(order_id, 'submitted',
f'백제 주문 확정 완료: {success_count}')
# 결과 메시지 업데이트
for r in results:
if r['status'] == 'success':
r['result_code'] = 'OK'
r['result_message'] = '주문 확정 완료'
else:
update_order_status(order_id, 'partial',
f'백제 장바구니 담김, 확정 실패: {confirm_result.get("error", "알 수 없는 오류")}')
except Exception as e:
logger.error(f"백제 주문 확정 오류: {e}")
update_order_status(order_id, 'partial',
f'백제 장바구니 담김, 확정 중 오류: {str(e)}')
elif success_count > 0:
update_order_status(order_id, 'pending',
f'백제 장바구니 추가 완료: {success_count}개 (확정 필요)')
else:
update_order_status(order_id, 'failed',
f'백제 주문 실패: {failed_count}')
# 응답 생성
if cart_only:
note = '백제약품 장바구니에 담김. 백제몰(ibjp.co.kr)에서 최종 확정 필요.'
else:
note = None
return {
'success': True,
'dry_run': dry_run,
'cart_only': cart_only,
'order_id': order_id,
'order_no': order['order_no'],
'wholesaler': 'baekje',
'total_items': len(items),
'success_count': success_count,
'failed_count': failed_count,
'results': results,
'note': note if not dry_run else None
}
except Exception as e:
logger.error(f"백제 주문 오류: {e}")
update_order_status(order_id, 'failed', str(e))
return {
'success': False,
'order_id': order_id,
'error': str(e)
}
# ─────────────────────────────────────────────
# AI 학습용 API
# ─────────────────────────────────────────────
@order_bp.route('/ai/training-data', methods=['GET'])
def api_ai_training_data():
"""
AI 학습용 데이터 추출
GET /api/order/ai/training-data?limit=1000
"""
limit = int(request.args.get('limit', 1000))
data = get_ai_training_data(limit)
return jsonify({
'success': True,
'count': len(data),
'data': data
})
@order_bp.route('/ai/usage-stats/<drug_code>', methods=['GET'])
def api_ai_usage_stats(drug_code):
"""
약품 사용량 통계 (AI 분석용)
GET /api/order/ai/usage-stats/670400830?days=30
"""
days = int(request.args.get('days', 30))
stats = get_usage_stats(drug_code, days)
return jsonify({
'success': True,
'stats': stats
})
@order_bp.route('/ai/order-pattern/<drug_code>', methods=['GET'])
def api_ai_order_pattern(drug_code):
"""
약품 주문 패턴 조회
GET /api/order/ai/order-pattern/670400830
"""
pattern = get_order_pattern(drug_code)
if pattern:
return jsonify({'success': True, 'pattern': pattern})
else:
return jsonify({
'success': True,
'pattern': None,
'message': '주문 이력이 없습니다'
})