diff --git a/backend/order_api.py b/backend/order_api.py index 36da2aa..18c330d 100644 --- a/backend/order_api.py +++ b/backend/order_api.py @@ -409,7 +409,7 @@ def api_quick_submit(): POST /api/order/quick-submit { - "wholesaler_id": "geoyoung", + "wholesaler_id": "geoyoung" | "sooin", "items": [...], "dry_run": true } @@ -441,14 +441,226 @@ def api_quick_submit(): if order['wholesaler_id'] == 'geoyoung': submit_result = submit_geoyoung_order(order, dry_run) + elif order['wholesaler_id'] == 'sooin': + submit_result = submit_sooin_order(order, dry_run) else: - submit_result = {'success': False, 'error': 'Wholesaler not supported'} + 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) -> dict: + """수인약품 주문 제출""" + order_id = order['id'] + items = order['items'] + + # 상태 업데이트 + update_order_status(order_id, 'pending', + f'수인 주문 시작 (dry_run={dry_run})') + + 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', '') + internal_code = item.get('internal_code') + + try: + # internal_code가 없으면 검색해서 찾기 + if not internal_code: + search_result = sooin_session.search_products(kd_code) + if search_result.get('success'): + for sooin_item in search_result.get('items', []): + if spec in sooin_item.get('spec', '') or sooin_item.get('spec', '') in spec: + internal_code = sooin_item.get('internal_code') + break + + if not internal_code: + raise ValueError(f"내부 코드를 찾을 수 없음: {kd_code} {spec}") + + # 장바구니 추가 + cart_result = sooin_session.add_to_cart(internal_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': '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 + }) + + # 주문 확정은 별도로 (장바구니에 담기만 한 상태) + if success_count > 0: + update_order_status(order_id, 'pending', + f'수인 장바구니 추가 완료: {success_count}개 (확정 필요)') + else: + update_order_status(order_id, 'failed', + f'수인 주문 실패: {failed_count}개') + + return { + 'success': True, + 'dry_run': dry_run, + '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': '실제 주문 시 장바구니에 담김. 수인약품 사이트에서 최종 확정 필요.' 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 # ───────────────────────────────────────────── diff --git a/backend/templates/admin_rx_usage.html b/backend/templates/admin_rx_usage.html index 8846ae8..a6a6df7 100644 --- a/backend/templates/admin_rx_usage.html +++ b/backend/templates/admin_rx_usage.html @@ -1052,25 +1052,62 @@ } // ──────────────── 주문 제출 ──────────────── + let currentOrderWholesaler = null; + function submitOrder() { if (cart.length === 0) return; - // 지오영 품목만 필터 - const geoItems = cart.filter(c => c.supplier === '지오영' || c.geoyoung_code); + // 도매상별 분류 + const geoItems = cart.filter(c => c.supplier === '지오영' || c.wholesaler === 'geoyoung'); + const sooinItems = cart.filter(c => c.supplier === '수인약품' || c.wholesaler === 'sooin'); + const otherItems = cart.filter(c => !geoItems.includes(c) && !sooinItems.includes(c)); - if (geoItems.length === 0) { - // 지오영 품목 없으면 기존 방식 (클립보드) + if (geoItems.length === 0 && sooinItems.length === 0) { + // API 지원 안 되는 품목만 있으면 클립보드 submitOrderClipboard(); return; } - // 지오영 주문 모달 열기 - openOrderConfirmModal(geoItems); + // 도매상 선택 모달 열기 (여러 도매상 품목이 있을 때) + if (geoItems.length > 0 && sooinItems.length > 0) { + openWholesalerSelectModal(geoItems, sooinItems, otherItems); + } else if (geoItems.length > 0) { + openOrderConfirmModal('geoyoung', geoItems); + } else if (sooinItems.length > 0) { + openOrderConfirmModal('sooin', sooinItems); + } } - function openOrderConfirmModal(items) { + function openWholesalerSelectModal(geoItems, sooinItems, otherItems) { + // 간단하게 지오영 먼저 처리 + const msg = `장바구니에 여러 도매상 품목이 있습니다.\n\n` + + `🏭 지오영: ${geoItems.length}개\n` + + `💊 수인약품: ${sooinItems.length}개\n` + + (otherItems.length > 0 ? `📋 기타: ${otherItems.length}개\n` : '') + + `\n어느 도매상부터 주문하시겠습니까?`; + + if (confirm(msg + '\n\n[확인] = 지오영 먼저\n[취소] = 수인약품 먼저')) { + openOrderConfirmModal('geoyoung', geoItems); + } else { + openOrderConfirmModal('sooin', sooinItems); + } + } + + function openOrderConfirmModal(wholesaler, items) { + currentOrderWholesaler = wholesaler; + const modal = document.getElementById('orderConfirmModal'); const tbody = document.getElementById('orderConfirmBody'); + const header = modal.querySelector('.order-modal-header h3'); + + // 도매상별 헤더 스타일 + if (wholesaler === 'sooin') { + header.innerHTML = '💊 수인약품 주문 확인'; + modal.querySelector('.order-modal-header').style.background = 'linear-gradient(135deg, #7c3aed, #a855f7)'; + } else { + header.innerHTML = '🏭 지오영 주문 확인'; + modal.querySelector('.order-modal-header').style.background = 'linear-gradient(135deg, #0891b2, #06b6d4)'; + } let html = ''; items.forEach((item, idx) => { @@ -1089,13 +1126,22 @@ function closeOrderConfirmModal() { document.getElementById('orderConfirmModal').classList.remove('show'); + currentOrderWholesaler = null; } async function executeOrder(dryRun = true) { - const geoItems = cart.filter(c => c.supplier === '지오영' || c.geoyoung_code); + const wholesaler = currentOrderWholesaler || 'geoyoung'; - if (geoItems.length === 0) { - showToast('지오영 품목이 없습니다', 'error'); + // 해당 도매상 품목 필터 + let items; + if (wholesaler === 'sooin') { + items = cart.filter(c => c.supplier === '수인약품' || c.wholesaler === 'sooin'); + } else { + items = cart.filter(c => c.supplier === '지오영' || c.wholesaler === 'geoyoung'); + } + + if (items.length === 0) { + showToast(`${wholesaler === 'sooin' ? '수인약품' : '지오영'} 품목이 없습니다`, 'error'); return; } @@ -1109,10 +1155,11 @@ try { const payload = { - wholesaler_id: 'geoyoung', - items: geoItems.map(item => ({ + wholesaler_id: wholesaler, + items: items.map(item => ({ drug_code: item.drug_code, - kd_code: item.geoyoung_code || item.drug_code, + kd_code: item.geoyoung_code || item.sooin_code || item.drug_code, + internal_code: item.internal_code, product_name: item.product_name, manufacturer: item.supplier, specification: item.specification || '', @@ -1124,8 +1171,8 @@ dry_run: dryRun }; - // 실제 주문은 시간이 오래 걸림 (Playwright 사용) - const timeoutMs = dryRun ? 60000 : 180000; // 테스트 1분, 실제 3분 + // 타임아웃 설정 + const timeoutMs = dryRun ? 120000 : 180000; // 테스트 2분, 실제 3분 const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs);