# -*- 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('/', 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/', 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/', 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': '주문 이력이 없습니다' })