# -*- coding: utf-8 -*- """ 지오영 도매상 API - Flask Blueprint 핵심 로직은 wholesale 패키지에서 가져옴 이 파일은 Flask 웹 API 연동만 담당 """ import re import time import logging from flask import Blueprint, jsonify, request # wholesale 패키지 경로 설정 import wholesale_path # wholesale 패키지에서 핵심 클래스 가져오기 from wholesale import GeoYoungSession logger = logging.getLogger(__name__) # Blueprint 생성 geoyoung_bp = Blueprint('geoyoung', __name__, url_prefix='/api/geoyoung') # ========== 세션 관리 ========== _geo_session = None def get_geo_session(): global _geo_session if _geo_session is None: _geo_session = GeoYoungSession() return _geo_session def search_geoyoung_stock(keyword: str, include_price: bool = True): """지오영 재고 검색 (동기, 단가 포함)""" try: session = get_geo_session() # 새 API 사용 (단가 포함) result = session.search_products(keyword, include_price=include_price) if result.get('success'): # 기존 형식으로 변환 items = [{ 'insurance_code': item['code'], 'internal_code': item.get('internal_code'), 'manufacturer': item['manufacturer'], 'product_name': item['name'], 'specification': item['spec'], 'stock': item['stock'], 'price': item.get('price', 0), # 단가 추가! 'box_qty': item.get('box_qty'), 'case_qty': item.get('case_qty') } for item in result['items']] return { 'success': True, 'keyword': keyword, 'count': len(items), 'items': items } else: return {'success': False, 'error': result.get('error'), 'message': '검색 실패'} except Exception as e: logger.error(f"지오영 검색 오류: {e}") return {'success': False, 'error': 'SEARCH_ERROR', 'message': str(e)} # ========== Flask API Routes ========== @geoyoung_bp.route('/stock', methods=['GET']) def api_geoyoung_stock(): """ 지오영 재고 조회 API (빠름) GET /api/geoyoung/stock?kd_code=670400830 GET /api/geoyoung/stock?keyword=레바미피드 """ kd_code = request.args.get('kd_code', '').strip() keyword = 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_geoyoung_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 @geoyoung_bp.route('/stock-by-name', methods=['GET']) def api_geoyoung_stock_by_name(): """ 제품명에서 성분명 추출 후 지오영 검색 GET /api/geoyoung/stock-by-name?product_name=휴니즈레바미피드정_(0.1g/1정) """ product_name = request.args.get('product_name', '').strip() if not product_name: return jsonify({ 'success': False, 'error': 'MISSING_PARAM', 'message': 'product_name 파라미터가 필요합니다' }), 400 # 성분명 추출 prefixes = ['휴니즈', '휴온스', '대웅', '한미', '종근당', '유한', '녹십자', '동아', '일동', '광동', '삼성', '안국', '보령', '광동', '경동', '현대', '일양', '태극', '환인', '에스케이'] ingredient = product_name for prefix in prefixes: if ingredient.startswith(prefix): ingredient = ingredient[len(prefix):] break match = re.match(r'^([가-힣a-zA-Z]+)', ingredient) if match: ingredient = match.group(1) if ingredient.endswith('정'): ingredient = ingredient[:-1] elif ingredient.endswith('캡슐'): ingredient = ingredient[:-2] if not ingredient: ingredient = product_name[:10] try: result = search_geoyoung_stock(ingredient) result['extracted_ingredient'] = ingredient result['original_product_name'] = product_name return jsonify(result) except Exception as e: logger.error(f"지오영 API 오류: {e}") return jsonify({ 'success': False, 'error': 'API_ERROR', 'message': str(e) }), 500 @geoyoung_bp.route('/session-status', methods=['GET']) def api_session_status(): """세션 상태 확인""" session = get_geo_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 }) @geoyoung_bp.route('/cart', methods=['GET']) def api_geoyoung_cart(): """장바구니 조회 API""" try: session = get_geo_session() result = session.get_cart() return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e), 'items': []}), 500 @geoyoung_bp.route('/cart/clear', methods=['POST']) def api_geoyoung_cart_clear(): """장바구니 비우기 API""" try: session = get_geo_session() result = session.clear_cart() return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @geoyoung_bp.route('/cart/cancel', methods=['POST']) def api_geoyoung_cart_cancel(): """ 장바구니 개별 항목 삭제 API (Hard delete) POST /api/geoyoung/cart/cancel { "row_index": 0, // 또는 "product_code": "008709" } ⚠️ 지오영은 완전 삭제됨 (복원 불가, 다시 추가해야 함) """ data = request.get_json() or {} row_index = data.get('row_index') product_code = data.get('product_code') if row_index is None and not product_code: return jsonify({ 'success': False, 'error': 'MISSING_PARAM', 'message': 'row_index 또는 product_code 필요' }), 400 try: session = get_geo_session() result = session.cancel_item(row_index=row_index, product_code=product_code) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @geoyoung_bp.route('/cart/restore', methods=['POST']) def api_geoyoung_cart_restore(): """ 삭제된 항목 복원 API - 지오영은 Hard delete이므로 지원 안 함 Returns: 항상 {'success': False, 'error': 'NOT_SUPPORTED'} """ return jsonify({ 'success': False, 'error': 'NOT_SUPPORTED', 'message': '지오영은 삭제 후 복원 불가 (다시 추가 필요)' }), 400 @geoyoung_bp.route('/confirm', methods=['POST']) def api_geoyoung_confirm(): """주문 확정 API""" data = request.get_json() or {} memo = data.get('memo', '') try: session = get_geo_session() result = session.submit_order(memo) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @geoyoung_bp.route('/full-order', methods=['POST']) def api_geoyoung_full_order(): """전체 주문 API (검색 → 장바구니 → 확정)""" data = request.get_json() if not data or not data.get('kd_code'): return jsonify({'success': False, 'error': 'kd_code required'}), 400 try: session = get_geo_session() result = session.full_order( kd_code=data['kd_code'], quantity=data.get('quantity', 1), specification=data.get('specification'), check_stock=data.get('check_stock', True), auto_confirm=data.get('auto_confirm', True), memo=data.get('memo', '') ) return jsonify(result) except Exception as e: return jsonify({'success': False, 'error': str(e)}), 500 @geoyoung_bp.route('/order', methods=['POST']) def api_geoyoung_order(): """지오영 주문 API (장바구니 추가)""" data = 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_geo_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 @geoyoung_bp.route('/order-batch', methods=['POST']) def api_geoyoung_order_batch(): """지오영 일괄 주문 API""" data = 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_geo_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 }) # ========== 잔고 탐색 (임시) ========== @geoyoung_bp.route('/explore-balance', methods=['GET']) def api_explore_balance(): """잔고 페이지 탐색 (임시 디버그용)""" from bs4 import BeautifulSoup session = get_geo_session() if not session._logged_in: session.login() results = { 'logged_in': session._logged_in, 'cookies': len(session.session.cookies), 'pages_found': [], 'balance_pages': [] } # Order 페이지에서 메뉴 링크 수집 try: # 먼저 Order 페이지 접근 resp = session.session.get(f"{session.BASE_URL}/Home/Order", timeout=10) results['order_page'] = { 'status': resp.status_code, 'url': resp.url, 'is_error': 'Error' in resp.url } if resp.status_code == 200 and 'Error' not in resp.url: soup = BeautifulSoup(resp.text, 'html.parser') # 모든 링크 추출 for link in soup.find_all('a', href=True): href = link.get('href', '') text = link.get_text(strip=True)[:50] if href.startswith('/') and href not in [l['href'] for l in results['pages_found']]: entry = {'href': href, 'text': text} results['pages_found'].append(entry) # 잔고 관련 키워드 keywords = ['account', 'balance', 'trans', 'state', 'history', 'ledger', '잔고', '잔액', '거래', '명세', '내역'] if any(kw in href.lower() or kw in text for kw in keywords): results['balance_pages'].append(entry) except Exception as e: results['error'] = str(e) return jsonify(results) @geoyoung_bp.route('/balance', methods=['GET']) def api_get_balance(): """ 잔고액 조회 GET /api/geoyoung/balance """ session = get_geo_session() # get_balance 메서드가 있으면 호출 if hasattr(session, 'get_balance'): result = session.get_balance() return jsonify(result) else: return jsonify({ 'success': False, 'error': 'NOT_IMPLEMENTED', 'message': '지오영 잔고 조회 미구현' }), 501 @geoyoung_bp.route('/monthly-sales', methods=['GET']) def api_get_monthly_sales(): """ 월간 매출 조회 GET /api/geoyoung/monthly-sales?year=2026&month=3 """ from datetime import datetime year = request.args.get('year', type=int) month = 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 session = get_geo_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 # ========== 주문 조회 API ========== @geoyoung_bp.route('/order-list', methods=['GET']) def api_geoyoung_order_list(): """ 지오영 주문 목록 조회 API GET /api/geoyoung/order-list?start_date=2026-03-01&end_date=2026-03-07 Query Parameters: start_date: 시작일 (YYYY-MM-DD), 기본값 30일 전 end_date: 종료일 (YYYY-MM-DD), 기본값 오늘 Returns: { "success": true, "orders": [{ "order_num": "DA2603-0006409", "order_date": "2026-03-07", "order_time": "09:08:55", "total_amount": 132020, "item_count": 3, "status": "출고확정" }, ...], "total_count": 5, "start_date": "2026-03-01", "end_date": "2026-03-07" } """ start_date = request.args.get('start_date', '').strip() end_date = request.args.get('end_date', '').strip() try: session = get_geo_session() result = session.get_order_list(start_date or None, end_date or None) return jsonify(result) except Exception as e: logger.error(f"지오영 주문 목록 조회 오류: {e}") return jsonify({ 'success': False, 'error': 'API_ERROR', 'message': str(e), 'orders': [], 'total_count': 0 }), 500 @geoyoung_bp.route('/order-detail/', methods=['GET']) def api_geoyoung_order_detail(order_num): """ 지오영 주문 상세 조회 API GET /api/geoyoung/order-detail/DA2603-0006409 Returns: { "success": true, "order_num": "DA2603-0006409", "order_date": "2026-03-07", "order_time": "09:08:55", "items": [{ "product_code": "008709", "kd_code": "670400830", "product_name": "레바미피드정100mg", "spec": "100mg", "quantity": 10, "unit_price": 500, "amount": 5000 }, ...], "total_amount": 132020, "item_count": 3 } """ try: session = get_geo_session() result = session.get_order_detail(order_num) return jsonify(result) except Exception as e: logger.error(f"지오영 주문 상세 조회 오류: {e}") return jsonify({ 'success': False, 'error': 'API_ERROR', 'message': str(e), 'order_num': order_num, 'items': [], 'total_amount': 0 }), 500 @geoyoung_bp.route('/orders/summary-by-kd', methods=['GET']) def api_geoyoung_orders_by_kd(): """ 지오영 주문량 KD코드별 집계 API GET /api/geoyoung/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 = request.args.get('start_date', today).strip() end_date = request.args.get('end_date', today).strip() def parse_spec(spec: str, product_name: str = '') -> int: """ 규격에서 수량 추출 (30T → 30, 100C → 100) 단위 처리: - T/C/P: 정/캡슐/포 → 숫자 그대로 (30T → 30) - D: 도즈/분사 → 1로 처리 (140D → 1, 박스 단위) - mg/ml/g: 용량 → 1로 처리 """ 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의 첫 번째 숫자 (mg, ml 등 용량일 수 있음 - 기본값 1) 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_geo_session() # 주문 목록 조회 (items 포함) 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': {}, 'order_count': 0 }) orders = orders_result.get('orders', []) # 각 주문의 items에 KD코드 추가 (enrich) for order in orders: items = order.get('items', []) if items: session._enrich_kd_codes(items) # KD코드별 집계 kd_summary = {} for order in orders: # 지오영은 get_order_list에서 items도 같이 반환 for item in order.get('items', []): # 취소/삭제 상태 제외 status = item.get('status', '').strip() if '취소' in status or '삭제' in status: continue 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) 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 logger.info(f"지오영 주문량 집계: {start_date}~{end_date}, {len(orders)}건 주문, {len(kd_summary)}개 품목") return jsonify({ 'success': True, 'order_count': len(orders), '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 @geoyoung_bp.route('/order-today', methods=['GET']) def api_geoyoung_order_today(): """ 지오영 오늘 주문 요약 API GET /api/geoyoung/order-today Returns: { "success": true, "date": "2026-03-07", "order_count": 3, "total_amount": 450000, "item_count": 15, "orders": [...] } """ try: session = get_geo_session() result = session.get_today_order_summary() return jsonify(result) except Exception as e: logger.error(f"지오영 오늘 주문 조회 오류: {e}") return jsonify({ 'success': False, 'error': 'API_ERROR', 'message': str(e), 'date': '', 'order_count': 0, 'total_amount': 0 }), 500 # ========== 하위 호환성 ========== # 기존 코드에서 직접 클래스 참조하는 경우를 위해 GeoyoungSession = GeoYoungSession