# -*- 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 SooinSession logger = logging.getLogger(__name__) # Blueprint 생성 sooin_bp = Blueprint('sooin', __name__, url_prefix='/api/sooin') # ========== 세션 관리 ========== _sooin_session = None def get_sooin_session(): global _sooin_session if _sooin_session is None: _sooin_session = SooinSession() return _sooin_session def search_sooin_stock(keyword: str, search_type: str = 'kd_code'): """수인약품 재고 검색 (동기, 빠름)""" try: session = get_sooin_session() result = session.search_products(keyword) if result.get('success'): return { 'success': True, 'keyword': keyword, 'search_type': search_type, '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 ========== @sooin_bp.route('/stock', methods=['GET']) def api_sooin_stock(): """ 수인약품 재고 조회 API GET /api/sooin/stock?kd_code=073100220 GET /api/sooin/stock?keyword=코자정&type=name """ kd_code = flask_request.args.get('kd_code', '').strip() keyword = flask_request.args.get('keyword', '').strip() search_type = flask_request.args.get('type', 'kd_code').strip() search_term = kd_code or keyword if kd_code: search_type = 'kd_code' if not search_term: return jsonify({ 'success': False, 'error': 'MISSING_PARAM', 'message': 'kd_code 또는 keyword 파라미터가 필요합니다' }), 400 try: result = search_sooin_stock(search_term, search_type) return jsonify(result) except Exception as e: logger.error(f"수인약품 API 오류: {e}") return jsonify({ 'success': False, 'error': 'API_ERROR', 'message': str(e) }), 500 @sooin_bp.route('/session-status', methods=['GET']) def api_session_status(): """세션 상태 확인""" session = get_sooin_session() return jsonify({ 'logged_in': session._logged_in, 'last_login': session._last_login, 'session_age_sec': int(time.time() - session._last_login) if session._last_login else None }) @sooin_bp.route('/balance', methods=['GET']) def api_sooin_balance(): """ 수인약품 잔고(미수금) 조회 API GET /api/sooin/balance Returns: { "success": true, "balance": 14293001, // 현재 잔고 (누계합) "prev_balance": 10592762, // 전일잔액 "monthly_sales": 3700239, // 월 매출 "yearly_sales": 34380314 // 연 누계 매출 } """ try: session = get_sooin_session() result = session.get_balance() return jsonify(result) except Exception as e: logger.error(f"수인약품 잔고 조회 오류: {e}") return jsonify({ 'success': False, 'error': 'BALANCE_ERROR', 'message': str(e), 'balance': 0 }), 500 @sooin_bp.route('/monthly-sales', methods=['GET']) def api_sooin_monthly_sales(): """ 수인약품 월간 매출 조회 API GET /api/sooin/monthly-sales?year=2026&month=3 Returns: { "success": true, "total_amount": 3700239, // 월간 매출 합계 "total_paid": 0, // 월간 입금 합계 "ending_balance": 14293001, // 월말 잔액 "opening_balance": 10592762, // 전일(기초) 잔액 "from_date": "2026-03-01", "to_date": "2026-03-31" } """ from datetime import datetime year = flask_request.args.get('year', type=int) month = flask_request.args.get('month', type=int) # 기본값: 현재 월 if not year or not month: now = datetime.now() year = year or now.year month = month or now.month try: session = get_sooin_session() if hasattr(session, 'get_monthly_sales'): result = session.get_monthly_sales(year, month) return jsonify(result) else: return jsonify({ 'success': False, 'error': 'NOT_IMPLEMENTED', 'message': '수인약품 월간 매출 조회 미구현' }), 501 except Exception as e: logger.error(f"수인약품 월간 매출 조회 오류: {e}") return jsonify({ 'success': False, 'error': 'MONTHLY_SALES_ERROR', 'message': str(e) }), 500 @sooin_bp.route('/cart', methods=['GET']) def api_sooin_cart(): """장바구니 조회 API""" try: session = get_sooin_session() result = session.get_cart() return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e), 'items': []}), 500 @sooin_bp.route('/cart/clear', methods=['POST']) def api_sooin_cart_clear(): """장바구니 비우기 API""" try: session = get_sooin_session() result = session.clear_cart() return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @sooin_bp.route('/cart/cancel', methods=['POST']) def api_sooin_cart_cancel(): """ 장바구니 항목 취소 API POST /api/sooin/cart/cancel { "row_index": 0 } 또는 { "internal_code": "32495" } """ data = flask_request.get_json() or {} row_index = data.get('row_index') internal_code = data.get('internal_code') if row_index is None and not internal_code: return jsonify({ 'success': False, 'error': 'MISSING_PARAM', 'message': 'row_index 또는 internal_code가 필요합니다' }), 400 try: session = get_sooin_session() result = session.cancel_item(row_index=row_index, product_code=internal_code) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @sooin_bp.route('/cart/restore', methods=['POST']) def api_sooin_cart_restore(): """ 취소된 항목 복원 API POST /api/sooin/cart/restore { "row_index": 0 } """ data = flask_request.get_json() or {} row_index = data.get('row_index') internal_code = data.get('internal_code') try: session = get_sooin_session() result = session.restore_item(row_index=row_index, product_code=internal_code) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @sooin_bp.route('/order', methods=['POST']) def api_sooin_order(): """ 수인약품 주문 API (장바구니 추가) POST /api/sooin/order { "kd_code": "073100220", "quantity": 1, "specification": "30T", "check_stock": true } """ data = flask_request.get_json() if not data: return jsonify({'success': False, 'error': 'NO_DATA'}), 400 kd_code = data.get('kd_code', '').strip() quantity = data.get('quantity', 1) specification = data.get('specification') check_stock = data.get('check_stock', True) if not kd_code: return jsonify({ 'success': False, 'error': 'MISSING_PARAM', 'message': 'kd_code가 필요합니다' }), 400 try: session = get_sooin_session() result = session.quick_order( kd_code=kd_code, quantity=quantity, spec=specification, check_stock=check_stock ) return jsonify(result) except Exception as e: logger.error(f"수인약품 주문 오류: {e}") return jsonify({ 'success': False, 'error': 'ORDER_ERROR', 'message': str(e) }), 500 @sooin_bp.route('/confirm', methods=['POST']) def api_sooin_confirm(): """주문 확정 API""" data = flask_request.get_json() or {} memo = data.get('memo', '') try: session = get_sooin_session() result = session.submit_order(memo) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @sooin_bp.route('/full-order', methods=['POST']) def api_sooin_full_order(): """ 전체 주문 API (검색 → 장바구니 → 확정) POST /api/sooin/full-order { "kd_code": "073100220", "quantity": 1, "specification": "30T", "auto_confirm": true, "memo": "자동주문" } """ data = flask_request.get_json() if not data or not data.get('kd_code'): return jsonify({'success': False, 'error': 'kd_code required'}), 400 try: session = get_sooin_session() # 장바구니에 담기 cart_result = session.quick_order( kd_code=data['kd_code'], quantity=data.get('quantity', 1), spec=data.get('specification'), check_stock=data.get('check_stock', True) ) if not cart_result.get('success'): return jsonify(cart_result) if not data.get('auto_confirm', True): return jsonify(cart_result) # 주문 확정 confirm_result = session.submit_order(data.get('memo', '')) if confirm_result.get('success'): return jsonify({ 'success': True, 'message': f"{cart_result['product']['name']} {cart_result['quantity']}개 주문 완료", 'product': cart_result['product'], 'quantity': cart_result['quantity'], 'confirmed': True }) else: return jsonify({ 'success': False, 'error': confirm_result.get('error', 'CONFIRM_FAILED'), 'message': f"장바구니 담기 성공, 주문 확정 실패", 'product': cart_result['product'], 'cart_added': True }) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @sooin_bp.route('/order-batch', methods=['POST']) def api_sooin_order_batch(): """수인약품 일괄 주문 API""" data = flask_request.get_json() if not data or not data.get('items'): return jsonify({'success': False, 'error': 'NO_ITEMS'}), 400 items = data.get('items', []) check_stock = data.get('check_stock', True) session = get_sooin_session() results = [] success_count = 0 failed_count = 0 for item in items: kd_code = item.get('kd_code', '').strip() quantity = item.get('quantity', 1) specification = item.get('specification') if not kd_code: results.append({ 'kd_code': kd_code, 'success': False, 'error': 'MISSING_KD_CODE' }) failed_count += 1 continue try: result = session.quick_order( kd_code=kd_code, quantity=quantity, spec=specification, check_stock=check_stock ) result['kd_code'] = kd_code result['requested_qty'] = quantity results.append(result) if result.get('success'): success_count += 1 else: failed_count += 1 except Exception as e: results.append({ 'kd_code': kd_code, 'success': False, 'error': 'EXCEPTION', 'message': str(e) }) failed_count += 1 return jsonify({ 'success': True, 'total': len(items), 'success_count': success_count, 'failed_count': failed_count, 'results': results }) # ========== 주문 조회 API ========== @sooin_bp.route('/orders', methods=['GET']) def api_sooin_orders(): """ 수인약품 주문 목록 조회 API GET /api/sooin/orders?start_date=2026-03-01&end_date=2026-03-07 파라미터: start_date: 시작일 (YYYY-MM-DD), 기본값: 오늘 end_date: 종료일 (YYYY-MM-DD), 기본값: 오늘 Returns: { "success": true, "orders": [ { "order_num": "202603095091177", "order_date": "2026-03-09", "order_time": "14:30:25", "total_amount": 125000, "item_count": 5, "status": "완료" } ], "total_count": 10 } """ 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() try: session = get_sooin_session() result = session.get_order_list(start_date, end_date) return jsonify(result) except Exception as e: logger.error(f"수인약품 주문 목록 조회 오류: {e}") return jsonify({ 'success': False, 'error': 'ORDERS_ERROR', 'message': str(e), 'orders': [], 'total_count': 0 }), 500 @sooin_bp.route('/orders/today-summary', methods=['GET']) def api_sooin_today_summary(): """ 수인약품 오늘 주문 집계 API GET /api/sooin/orders/today-summary Returns: { "success": true, "date": "2026-03-09", "summary": [ { "kd_code": "073100220", "product_name": "코자정50mg", "total_quantity": 10, "total_amount": 150000, "order_count": 3 } ], "grand_total_amount": 500000, "grand_total_items": 25, "order_count": 5 } """ try: session = get_sooin_session() result = session.get_today_order_summary() return jsonify(result) except Exception as e: logger.error(f"수인약품 오늘 주문 집계 오류: {e}") return jsonify({ 'success': False, 'error': 'TODAY_SUMMARY_ERROR', 'message': str(e), 'summary': [], 'grand_total_amount': 0, 'grand_total_items': 0, 'order_count': 0 }), 500 @sooin_bp.route('/orders/', methods=['GET']) def api_sooin_order_detail(order_num): """ 수인약품 주문 상세 조회 API GET /api/sooin/orders/202603095091177 Returns: { "success": true, "order_num": "202603095091177", "order_date": "2026-03-09", "items": [ { "product_code": "32495", "kd_code": "073100220", "product_name": "코자정50mg", "spec": "30T", "quantity": 2, "unit_price": 15000, "amount": 30000 } ], "total_amount": 125000, "item_count": 5 } """ if not order_num or not order_num.isdigit(): return jsonify({ 'success': False, 'error': 'INVALID_ORDER_NUM', 'message': '유효한 주문번호를 입력하세요' }), 400 try: session = get_sooin_session() result = session.get_order_detail(order_num) return jsonify(result) except Exception as e: logger.error(f"수인약품 주문 상세 조회 오류: {e}") return jsonify({ 'success': False, 'error': 'ORDER_DETAIL_ERROR', 'message': str(e), 'order_num': order_num, 'items': [], 'total_amount': 0 }), 500 @sooin_bp.route('/orders/summary-by-kd', methods=['GET']) def api_sooin_orders_by_kd(): """ 수인약품 주문량 KD코드별 집계 API (병렬 처리) GET /api/sooin/orders/summary-by-kd?start_date=2026-03-01&end_date=2026-03-07 """ 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) -> int: """ 규격에서 박스당 단위 수 추출 정량 단위 (T, 정, 캡슐, C, PTP, 포 등): 숫자 추출 용량 단위 (g, ml, mL, mg, L 등): 1 반환 (튜브/병 단위) 예시: - '30T' → 30 (정제 30정) - '100정(PTP)' → 100 - '15g' → 1 (튜브 1개) - '10ml' → 1 (병 1개) - '500mg' → 1 (용량 표시) """ if not spec: return 1 spec_lower = spec.lower() # 용량 단위 패턴: 숫자 + g/ml/mg/l (단독 또는 끝) # 이 경우 튜브/병 단위이므로 1 반환 volume_pattern = r'^\d+\s*(g|ml|mg|l)(\s|$|\)|/)' if re.search(volume_pattern, spec_lower): return 1 # 정량 단위 패턴: 숫자 + T/정/캡슐/C/PTP/포 qty_pattern = r'(\d+)\s*(t|정|캡슐?|c|ptp|포|tab|cap)' qty_match = re.search(qty_pattern, spec_lower) if qty_match: return int(qty_match.group(1)) # 기본: 숫자만 있으면 추출하되, 용량 단위 재확인 # 끝에 g/ml이 있으면 1 반환 if re.search(r'\d+(g|ml)$', spec_lower): return 1 # 그 외 숫자 추출 match = re.search(r'(\d+)', spec) return int(match.group(1)) if match else 1 try: session = get_sooin_session() # 주문 목록 조회 orders_result = session.get_order_list(start_date, end_date) if not orders_result.get('success'): return jsonify({ 'success': False, 'error': orders_result.get('error', 'ORDERS_FETCH_FAILED'), 'by_kd_code': {} }) orders = orders_result.get('orders', []) order_nums = [o.get('order_num') for o in orders if o.get('order_num')] # 순차 처리 + 캐시 (캐시 효과 극대화) all_details = [] for order_num in order_nums: try: detail = session.get_order_detail(order_num) if detail.get('success'): all_details.append(detail) except Exception as e: logger.warning(f"주문 상세 조회 실패: {e}") # KD코드별 집계 kd_summary = {} for detail in all_details: for item in detail.get('items', []): kd_code = item.get('kd_code', '') if not kd_code: continue product_name = item.get('product_name', '') spec = item.get('spec', '') quantity = item.get('quantity', 0) per_unit = parse_spec(spec) 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 return jsonify({ 'success': True, 'order_count': len(order_nums), 'period': {'start': start_date, 'end': end_date}, 'by_kd_code': kd_summary, 'total_products': len(kd_summary) }) except Exception as e: logger.error(f"수인약품 KD코드별 집계 오류: {e}") return jsonify({ 'success': False, 'error': 'SUMMARY_ERROR', 'message': str(e), 'by_kd_code': {} }), 500