# -*- 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 _init_started = False def get_baekje_session(): global _baekje_session if _baekje_session is None: _baekje_session = BaekjeSession() return _baekje_session def init_baekje_session(): """앱 시작 시 백그라운드에서 로그인 시작""" global _init_started if _init_started: return _init_started = True session = get_baekje_session() # 저장된 토큰이 있으면 즉시 사용 가능 if session._logged_in: logger.info(f"백제약품: 저장된 토큰 사용 중") return # 백그라운드 로그인 시작 session.start_background_login() logger.info(f"백제약품: 백그라운드 로그인 시작됨") # 모듈 로드 시 자동 시작 try: init_baekje_session() except Exception as e: logger.warning(f"백제약품 초기화 오류: {e}") 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) # ========== 잔고 조회 ========== @baekje_bp.route('/balance', methods=['GET']) def api_get_balance(): """ 잔고액 조회 GET /api/baekje/balance GET /api/baekje/balance?year=2026 Returns: { "success": true, "balance": 14193234, "monthly": [ {"month": "2026-03", "sales": 6935133, "balance": 14193234, ...}, {"month": "2026-02", "sales": 18600692, "balance": 7258101, ...} ] } """ year = flask_request.args.get('year', '').strip() session = get_baekje_session() result = session.get_balance(year if year else None) return jsonify(result) @baekje_bp.route('/orders/summary-by-kd', methods=['GET']) def api_baekje_orders_by_kd(): """ 백제약품 주문량 KD코드별 집계 API GET /api/baekje/orders/summary-by-kd?start_date=2026-03-01&end_date=2026-03-07 Returns: { "success": true, "order_count": 4, "by_kd_code": { "670400830": { "product_name": "레바미피드정", "spec": "100T", "boxes": 2, "units": 200 } }, "total_products": 15 } """ import re from datetime import datetime today = datetime.now().strftime("%Y-%m-%d") start_date = flask_request.args.get('start_date', today).strip() end_date = flask_request.args.get('end_date', today).strip() def parse_spec(spec: str, product_name: str = '') -> int: """ 규격에서 수량 추출 (30T → 30, 100C → 100) """ combined = f"{spec} {product_name}" # D(도즈) 단위는 박스 단위로 계산 (140D → 1) if re.search(r'\d+\s*D\b', combined, re.IGNORECASE): return 1 # T/C/P 단위가 붙은 숫자 추출 (예: 14T, 100C, 30P) qty_match = re.search(r'(\d+)\s*[TCP]\b', combined, re.IGNORECASE) if qty_match: return int(qty_match.group(1)) # 없으면 spec의 첫 번째 숫자 if spec: num_match = re.search(r'(\d+)', spec) if num_match: val = int(num_match.group(1)) # mg, ml 같은 용량 단위면 수량 1로 처리 if re.search(r'\d+\s*(mg|ml|g)\b', spec, re.IGNORECASE): return 1 return val return 1 try: session = get_baekje_session() # 주문 목록 + 상세를 한 번에 조회 (include_details=True) # 접수 상태(확정 전)도 포함됨! orders_result = session.get_order_list(start_date, end_date, include_details=True) if not orders_result.get('success'): return jsonify({ 'success': False, 'error': orders_result.get('error', 'ORDERS_FETCH_FAILED'), 'by_kd_code': {}, 'order_count': 0 }) orders = orders_result.get('orders', []) if not orders: return jsonify({ 'success': True, 'order_count': 0, 'period': {'start': start_date, 'end': end_date}, 'by_kd_code': {}, 'total_products': 0, 'pending_count': 0, 'approved_count': 0 }) # KD코드별 집계 (items가 이미 각 order에 포함됨) kd_summary = {} for order in orders: for item in order.get('items', []): # 취소 상태 제외 status = item.get('status', '').strip() if '취소' in status or '삭제' in status: continue # 백제는 kd_code가 insurance_code(BOHUM_CD)에 있음 kd_code = item.get('kd_code', '') or item.get('insurance_code', '') if not kd_code: continue product_name = item.get('product_name', '') spec = item.get('spec', '') quantity = item.get('quantity', 0) or item.get('order_qty', 0) per_unit = parse_spec(spec, product_name) total_units = quantity * per_unit if kd_code not in kd_summary: kd_summary[kd_code] = { 'product_name': product_name, 'spec': spec, 'boxes': 0, 'units': 0 } kd_summary[kd_code]['boxes'] += quantity kd_summary[kd_code]['units'] += total_units pending_count = orders_result.get('pending_count', 0) approved_count = orders_result.get('approved_count', 0) logger.info(f"백제 주문량 집계: {start_date}~{end_date}, {len(orders)}건 (접수:{pending_count}, 승인:{approved_count}), {len(kd_summary)}개 품목") return jsonify({ 'success': True, 'order_count': len(orders), 'pending_count': pending_count, # 접수 상태 (확정 전) 'approved_count': approved_count, # 승인 상태 (확정됨) 'period': {'start': start_date, 'end': end_date}, 'by_kd_code': kd_summary, 'total_products': len(kd_summary) }) except Exception as e: logger.error(f"백제 주문량 집계 오류: {e}") return jsonify({ 'success': False, 'error': 'API_ERROR', 'message': str(e), 'by_kd_code': {}, 'order_count': 0 }), 500 @baekje_bp.route('/monthly-sales', methods=['GET']) def api_get_monthly_sales(): """ 월간 매출(주문) 합계 조회 GET /api/baekje/monthly-sales?year=2026&month=3 Returns: { "success": true, "total_amount": 7305877, // 월간 매출 합계 "total_returns": 0, // 월간 반품 합계 "net_amount": 7305877, // 순매출 (매출 - 반품) "total_paid": 0, // 월간 입금 합계 "ending_balance": 14563978, // 월말 잔액 "prev_balance": 14565453, // 전월이월금 "from_date": "2026-03-01", "to_date": "2026-03-31", "rotate_days": 58.4 // 회전일수 } """ from datetime import datetime year = flask_request.args.get('year', '').strip() month = flask_request.args.get('month', '').strip() # 기본값: 현재 연월 now = datetime.now() if not year: year = now.year else: year = int(year) if not month: month = now.month else: month = int(month) session = get_baekje_session() result = session.get_monthly_sales(year, month) return jsonify(result)