feat: 도매상 API 통합 및 스키마 업데이트

- wholesale 패키지 연동 (SooinSession, GeoYoungSession)
- Flask Blueprint 분리 (sooin_api.py, geoyoung_api.py)
- order_context 스키마 확장 (wholesaler_id, internal_code 등)
- 수인약품 개별 취소 기능 (cancel_item, restore_item)
- 문서 추가: WHOLESALE_API_INTEGRATION.md
- 테스트 스크립트들
This commit is contained in:
thug0bin
2026-03-06 11:50:46 +09:00
parent e84eda928a
commit c1596a6d35
53 changed files with 8789 additions and 3 deletions

316
backend/geoyoung_api.py Normal file
View File

@@ -0,0 +1,316 @@
# -*- 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):
"""지오영 재고 검색 (동기, 빠름)"""
try:
session = get_geo_session()
products = session.search_stock(keyword)
return {
'success': True,
'keyword': keyword,
'count': len(products),
'items': products
}
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('/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
})
# ========== 하위 호환성 ==========
# 기존 코드에서 직접 클래스 참조하는 경우를 위해
GeoyoungSession = GeoYoungSession