From 857a0586919f12f81e9634cf44c38806376376ac Mon Sep 17 00:00:00 2001 From: thug0bin Date: Fri, 6 Mar 2026 13:04:07 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=B0=B1=EC=A0=9C=EC=95=BD=ED=92=88=20?= =?UTF-8?q?API=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - baekje_api.py: Flask Blueprint 추가 - order_api.py: submit_baekje_order 함수 추가 - admin_rx_usage.html: WHOLESALERS에 baekje 추가 - 환경변수: BAEKJE_USER_ID, BAEKJE_PASSWORD URL: https://ibjp.co.kr (약국용 웹 주문 시스템) --- backend/app.py | 3 + backend/baekje_api.py | 208 ++++++++++++++++++++++++++ backend/order_api.py | 198 ++++++++++++++++++++++++ backend/templates/admin_rx_usage.html | 11 +- 4 files changed, 419 insertions(+), 1 deletion(-) create mode 100644 backend/baekje_api.py diff --git a/backend/app.py b/backend/app.py index 39f529a..bd29ab6 100644 --- a/backend/app.py +++ b/backend/app.py @@ -65,6 +65,9 @@ app.register_blueprint(geoyoung_bp) from sooin_api import sooin_bp app.register_blueprint(sooin_bp) +from baekje_api import baekje_bp +app.register_blueprint(baekje_bp) + from order_api import order_bp app.register_blueprint(order_bp) diff --git a/backend/baekje_api.py b/backend/baekje_api.py new file mode 100644 index 0000000..94e70a0 --- /dev/null +++ b/backend/baekje_api.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +""" +백제약품 도매상 API - Flask Blueprint + +핵심 로직은 wholesale 패키지에서 가져옴 +이 파일은 Flask 웹 API 연동만 담당 +""" + +import time +import logging + +from flask import Blueprint, jsonify, request as flask_request + +# wholesale 패키지 경로 설정 +import wholesale_path + +# wholesale 패키지에서 핵심 클래스 가져오기 +from wholesale import BaekjeSession + +logger = logging.getLogger(__name__) + +# Blueprint 생성 +baekje_bp = Blueprint('baekje', __name__, url_prefix='/api/baekje') + + +# ========== 세션 관리 ========== + +_baekje_session = None + +def get_baekje_session(): + global _baekje_session + if _baekje_session is None: + _baekje_session = BaekjeSession() + return _baekje_session + + +def search_baekje_stock(keyword: str): + """백제약품 재고 검색""" + try: + session = get_baekje_session() + result = session.search_products(keyword) + + if result.get('success'): + return { + 'success': True, + 'keyword': keyword, + 'count': result['total'], + 'items': result['items'] + } + else: + return result + + except Exception as e: + logger.error(f"백제약품 검색 오류: {e}") + return {'success': False, 'error': 'SEARCH_ERROR', 'message': str(e)} + + +# ========== Flask API Routes ========== + +@baekje_bp.route('/stock', methods=['GET']) +def api_baekje_stock(): + """ + 백제약품 재고 조회 API + + GET /api/baekje/stock?kd_code=672300240 + GET /api/baekje/stock?keyword=타이레놀 + """ + kd_code = flask_request.args.get('kd_code', '').strip() + keyword = flask_request.args.get('keyword', '').strip() + + search_term = kd_code or keyword + + if not search_term: + return jsonify({ + 'success': False, + 'error': 'MISSING_PARAM', + 'message': 'kd_code 또는 keyword 파라미터가 필요합니다' + }), 400 + + try: + result = search_baekje_stock(search_term) + return jsonify(result) + except Exception as e: + logger.error(f"백제약품 API 오류: {e}") + return jsonify({ + 'success': False, + 'error': 'API_ERROR', + 'message': str(e) + }), 500 + + +@baekje_bp.route('/session-status', methods=['GET']) +def api_session_status(): + """세션 상태 확인""" + session = get_baekje_session() + return jsonify({ + 'success': True, + 'wholesaler': 'baekje', + 'name': '백제약품', + 'logged_in': session._logged_in, + 'last_login': session._last_login, + 'session_timeout': session.SESSION_TIMEOUT + }) + + +@baekje_bp.route('/login', methods=['POST']) +def api_login(): + """수동 로그인""" + session = get_baekje_session() + success = session.login() + return jsonify({ + 'success': success, + 'message': '로그인 성공' if success else '로그인 실패' + }) + + +@baekje_bp.route('/cart', methods=['GET']) +def api_get_cart(): + """장바구니 조회""" + session = get_baekje_session() + result = session.get_cart() + return jsonify(result) + + +@baekje_bp.route('/cart', methods=['POST']) +def api_add_to_cart(): + """ + 장바구니 추가 + + POST /api/baekje/cart + { + "product_code": "672300240", + "quantity": 2 + } + """ + data = flask_request.get_json() or {} + product_code = data.get('product_code', '').strip() + quantity = int(data.get('quantity', 1)) + + if not product_code: + return jsonify({ + 'success': False, + 'error': 'MISSING_PARAM', + 'message': 'product_code 필요' + }), 400 + + session = get_baekje_session() + result = session.add_to_cart(product_code, quantity) + return jsonify(result) + + +@baekje_bp.route('/order', methods=['POST']) +def api_submit_order(): + """ + 주문 등록 + + POST /api/baekje/order + { + "memo": "긴급 요청" + } + """ + data = flask_request.get_json() or {} + memo = data.get('memo', '') + + session = get_baekje_session() + result = session.submit_order(memo) + return jsonify(result) + + +# ========== 프론트엔드 통합용 ========== + +@baekje_bp.route('/search-for-order', methods=['POST']) +def api_search_for_order(): + """ + 발주용 재고 검색 (프론트엔드 통합용) + + POST /api/baekje/search-for-order + { + "kd_code": "672300240", + "product_name": "타이레놀", + "specification": "500T" + } + """ + data = flask_request.get_json() or {} + kd_code = data.get('kd_code', '').strip() + product_name = data.get('product_name', '').strip() + specification = data.get('specification', '').strip() + + search_term = kd_code or product_name + + if not search_term: + return jsonify({ + 'success': False, + 'error': 'MISSING_PARAM' + }), 400 + + result = search_baekje_stock(search_term) + + if result.get('success') and specification: + # 규격 필터링 + filtered = [ + item for item in result.get('items', []) + if specification.lower() in item.get('spec', '').lower() + ] + result['items'] = filtered + result['count'] = len(filtered) + + return jsonify(result) diff --git a/backend/order_api.py b/backend/order_api.py index 18c330d..a8ad0af 100644 --- a/backend/order_api.py +++ b/backend/order_api.py @@ -443,6 +443,8 @@ def api_quick_submit(): submit_result = submit_geoyoung_order(order, dry_run) elif order['wholesaler_id'] == 'sooin': submit_result = submit_sooin_order(order, dry_run) + elif order['wholesaler_id'] == 'baekje': + submit_result = submit_baekje_order(order, dry_run) else: submit_result = {'success': False, 'error': f"Wholesaler {order['wholesaler_id']} not supported"} @@ -661,6 +663,202 @@ def submit_sooin_order(order: dict, dry_run: bool) -> dict: } +def submit_baekje_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 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 + }) + + # 상태 업데이트 + 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': 'baekje', + 'total_items': len(items), + 'success_count': success_count, + 'failed_count': failed_count, + 'results': results, + 'note': '실제 주문 시 장바구니에 담김. 백제몰(ibjp.co.kr)에서 최종 확정 필요.' 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 3d6f81d..d1202c0 100644 --- a/backend/templates/admin_rx_usage.html +++ b/backend/templates/admin_rx_usage.html @@ -1072,8 +1072,17 @@ gradient: 'linear-gradient(135deg, #7c3aed, #a855f7)', filterFn: (item) => item.supplier === '수인약품' || item.wholesaler === 'sooin', getCode: (item) => item.sooin_code || item.drug_code + }, + baekje: { + id: 'baekje', + name: '백제약품', + icon: '💉', + logo: '/static/img/logo_baekje.png', + color: '#f59e0b', + gradient: 'linear-gradient(135deg, #d97706, #f59e0b)', + filterFn: (item) => item.supplier === '백제약품' || item.wholesaler === 'baekje', + getCode: (item) => item.baekje_code || item.drug_code } - // 향후 추가: baekje, etc. }; // ──────────────── 주문 제출 ────────────────