454 lines
14 KiB
Python
454 lines
14 KiB
Python
# -*- 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
|
|
|
|
|
|
# ========== 하위 호환성 ==========
|
|
|
|
# 기존 코드에서 직접 클래스 참조하는 경우를 위해
|
|
GeoyoungSession = GeoYoungSession
|